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

+98
View File
@@ -0,0 +1,98 @@
package agent
import (
"context"
"errors"
"io"
"testing"
"git.gopher.ink/mirrors2/AutoZero/llm"
)
// 回合收尾钩子挂在 Run 的唯一出口 finishTurn 上。这条钩子是"工作区安全网"的
// 落点(进展沉淀、checkpoint 留档):入口各写各的就会漏一端——历史上
// recordProgress 就只有 webChat 与 cron 调过,终端一直没做。
// 所以这里钉三件事:成功一次、失败也一次、子 agent 不重复触发。
func TestOnTurnEndFiresOncePerRun(t *testing.T) {
a, _ := newTestAgent(t)
var got []error
a.Opt.OnTurnEnd = func(r TurnReport) { got = append(got, r.Err) }
a.Provider = &turnEndProvider{reply: "做完了"}
if err := a.Run(context.Background(), "把这件事做完"); err != nil {
t.Fatal(err)
}
if len(got) != 1 || got[0] != nil {
t.Errorf("收尾钩子 = %v, want 恰好一次且 err 为 nil", got)
}
}
// 失败的一轮同样要回调(宿主自己决定不做事),但拿到的必须是那一轮的 error,
// 不是 nil——留档逻辑靠这个区分"做到一半崩了"。
func TestOnTurnEndReportsFailure(t *testing.T) {
a, _ := newTestAgent(t)
boom := errors.New("gateway exploded")
var got []error
a.Opt.OnTurnEnd = func(r TurnReport) { got = append(got, r.Err) }
a.Provider = &turnEndProvider{err: boom}
if err := a.Run(context.Background(), "办不成"); err == nil {
t.Fatal("该失败")
}
if len(got) != 1 || !errors.Is(got[0], boom) {
t.Errorf("收尾钩子拿到 %v, want [%v]", got, boom)
}
}
// 子 agent 不触发:它的会话是 ephemeral,留档与进展归父级那一次回合,
// 否则一次委派就在工作区里多打一个 commit。
func TestOnTurnEndSkipsSubAgent(t *testing.T) {
a, _ := newTestAgent(t)
calls := 0
a.Opt.OnTurnEnd = func(TurnReport) { calls++ }
a.Provider = &turnEndProvider{reply: "子过程结束"}
a.Depth = 1
a.Opt.Stderr = io.Discard
if err := a.Run(context.Background(), "子任务"); err != nil {
t.Fatal(err)
}
if calls != 0 {
t.Errorf("子 agent 触发了收尾钩子 %d 次,want 0", calls)
}
}
// Notify 是宿主侧那句话的入口:没接就安静地什么都不做(不能因为没人收就 panic)。
func TestNotifyWithoutHost(t *testing.T) {
a, _ := newTestAgent(t)
a.Notify("没人听", false)
var got []string
a.Opt.OnNotice = func(text string, failed bool) {
if failed {
text += "!"
}
got = append(got, text)
}
a.Notify("留档 a1b2c3d", false)
a.Notify("压缩没压下去", true)
if len(got) != 2 || got[0] != "留档 a1b2c3d" || got[1] != "压缩没压下去!" {
t.Errorf("OnNotice 收到 %v", got)
}
}
// turnEndProvider 是一问一答的假网关:不产工具调用,所以 Run 一步就收尾。
// (包内已有的 stubProvider 没有失败路径,这里要能按用例造错误。)
type turnEndProvider struct {
reply string
err error
}
func (p *turnEndProvider) Stream(context.Context, string, []llm.Message, []llm.ToolDef,
llm.StreamOptions, func(llm.StreamDelta)) (llm.Message, error) {
if p.err != nil {
return llm.Message{}, p.err
}
return llm.Message{Role: llm.RoleAssistant, Content: p.reply}, nil
}