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,164 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.gopher.ink/mirrors2/AutoZero/llm"
|
||||
"git.gopher.ink/mirrors2/AutoZero/tools"
|
||||
)
|
||||
|
||||
// TestToolEventMarksSideEffects:副作用标记要随工具事件传给宿主,前端才能在
|
||||
// 回合失败时禁用自动重试。
|
||||
func TestToolEventMarksSideEffects(t *testing.T) {
|
||||
prov := &scriptedProvider{fn: func(call int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
|
||||
if call == 1 {
|
||||
return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{
|
||||
{ID: "r1", Name: "rotool", Input: "{}"},
|
||||
{ID: "s1", Name: "sidetool", Input: "{}"},
|
||||
}}, nil
|
||||
}
|
||||
return llm.Message{Role: llm.RoleAssistant, Content: "done"}, nil
|
||||
}}
|
||||
ro := &tools.Tool{Name: "rotool", ReadOnly: true, ConcurrencySafe: true, Run: func(context.Context, string) (tools.Result, error) {
|
||||
return tools.TextResult("r"), nil
|
||||
}}
|
||||
side := &tools.Tool{Name: "sidetool", SideEffect: true, Kind: tools.KindExec, Run: func(context.Context, string) (tools.Result, error) {
|
||||
return tools.TextResult("s"), nil
|
||||
}}
|
||||
a, _ := newTestAgent(t, ro, side)
|
||||
a.Provider = prov
|
||||
started := map[string]bool{}
|
||||
a.Opt.OnTool = func(ev ToolEvent) {
|
||||
if !ev.Done {
|
||||
started[ev.Name] = ev.SideEffect
|
||||
}
|
||||
}
|
||||
if err := a.Run(context.Background(), "go"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !started["sidetool"] {
|
||||
t.Error("side-effect tool must be marked in ToolEvent")
|
||||
}
|
||||
if started["rotool"] {
|
||||
t.Error("read-only tool must not be marked side-effect")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSideEffectsDisableWholeTurnRetry:副作用已经执行后,回合失败要带
|
||||
// ErrAfterSideEffects,宿主据此只给手动重试;工具本身只跑一次。
|
||||
func TestSideEffectsDisableWholeTurnRetry(t *testing.T) {
|
||||
runs := 0
|
||||
calls := 0
|
||||
prov := &scriptedProvider{fn: func(call int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{
|
||||
{ID: "s1", Name: "sidetool", Input: "{}"},
|
||||
}}, nil
|
||||
}
|
||||
return llm.Message{}, errors.New("boom")
|
||||
}}
|
||||
side := &tools.Tool{Name: "sidetool", SideEffect: true, Kind: tools.KindExec, Run: func(context.Context, string) (tools.Result, error) {
|
||||
runs++
|
||||
return tools.TextResult("done"), nil
|
||||
}}
|
||||
a, _ := newTestAgent(t, side)
|
||||
a.Provider = prov
|
||||
|
||||
err := a.Run(context.Background(), "go")
|
||||
if !errors.Is(err, ErrAfterSideEffects) {
|
||||
t.Fatalf("err = %v, want ErrAfterSideEffects", err)
|
||||
}
|
||||
if runs != 1 {
|
||||
t.Fatalf("side effect ran %d times, want 1", runs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSideEffectResultIsDurableBeforeNextTool:同一批串行工具里,前一个副作用
|
||||
// 工具的结果必须在后一个开始前就进会话——崩溃/重试时才能知道它已经跑过。
|
||||
func TestSideEffectResultIsDurableBeforeNextTool(t *testing.T) {
|
||||
var sessHasPrior bool
|
||||
var sessionMessages func() []llm.Message
|
||||
prov := &scriptedProvider{fn: func(call int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
|
||||
if call == 1 {
|
||||
return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{
|
||||
{ID: "t1", Name: "first", Input: "{}"},
|
||||
{ID: "t2", Name: "second", Input: "{}"},
|
||||
}}, nil
|
||||
}
|
||||
return llm.Message{Role: llm.RoleAssistant, Content: "done"}, nil
|
||||
}}
|
||||
first := &tools.Tool{Name: "first", SideEffect: true, Kind: tools.KindExec, Run: func(context.Context, string) (tools.Result, error) {
|
||||
return tools.TextResult("first-result"), nil
|
||||
}}
|
||||
second := &tools.Tool{Name: "second", SideEffect: true, Kind: tools.KindExec, Run: func(context.Context, string) (tools.Result, error) {
|
||||
for _, m := range sessionMessages() {
|
||||
if m.Role == llm.RoleTool && m.ToolCallID == "t1" {
|
||||
sessHasPrior = true
|
||||
}
|
||||
}
|
||||
return tools.TextResult("second-result"), nil
|
||||
}}
|
||||
a, s := newTestAgent(t, first, second)
|
||||
a.Provider = prov
|
||||
sessionMessages = func() []llm.Message { return s.Messages }
|
||||
|
||||
if err := a.Run(context.Background(), "go"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !sessHasPrior {
|
||||
t.Fatal("second tool ran before the first tool's result was persisted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResumeKeepsSideEffectFence:续跑一个已经执行过副作用工具的回合时,
|
||||
// 失败仍然不能自动整轮重试——历史里的 tool 结果就是证据。
|
||||
func TestResumeKeepsSideEffectFence(t *testing.T) {
|
||||
prov := &scriptedProvider{fn: func(int, string, []llm.Message, []llm.ToolDef) (llm.Message, error) {
|
||||
return llm.Message{}, errors.New("boom")
|
||||
}}
|
||||
side := &tools.Tool{Name: "sidetool", SideEffect: true, Kind: tools.KindExec, Run: func(context.Context, string) (tools.Result, error) {
|
||||
t.Error("resume must not re-run the side-effect tool")
|
||||
return tools.TextResult(""), nil
|
||||
}}
|
||||
a, sess := newTestAgent(t, side)
|
||||
a.Provider = prov
|
||||
sess.Append(llm.Message{Role: llm.RoleUser, Content: "go"})
|
||||
sess.Append(llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{{ID: "t1", Name: "sidetool"}}})
|
||||
sess.Append(llm.Message{Role: llm.RoleTool, ToolCallID: "t1", Tool: "sidetool", Content: "ok"})
|
||||
|
||||
err := a.Run(context.Background(), "")
|
||||
if !errors.Is(err, ErrAfterSideEffects) {
|
||||
t.Fatalf("err = %v, want ErrAfterSideEffects", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSideEffectsDoNotWrapCancellation:用户主动停止必须保持 context.Canceled,
|
||||
// 不能被副作用标记包成普通错误——界面靠它区分"停止"和"出错"。
|
||||
func TestSideEffectsDoNotWrapCancellation(t *testing.T) {
|
||||
calls := 0
|
||||
prov := &scriptedProvider{fn: func(int, string, []llm.Message, []llm.ToolDef) (llm.Message, error) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{
|
||||
{ID: "s1", Name: "sidetool", Input: "{}"},
|
||||
}}, nil
|
||||
}
|
||||
return llm.Message{}, context.Canceled
|
||||
}}
|
||||
side := &tools.Tool{Name: "sidetool", SideEffect: true, Kind: tools.KindExec, Run: func(context.Context, string) (tools.Result, error) {
|
||||
return tools.TextResult("done"), nil
|
||||
}}
|
||||
a, _ := newTestAgent(t, side)
|
||||
a.Provider = prov
|
||||
|
||||
err := a.Run(context.Background(), "go")
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("err = %v, want context.Canceled", err)
|
||||
}
|
||||
if errors.Is(err, ErrAfterSideEffects) {
|
||||
t.Fatalf("cancellation must not be wrapped as a retryable failure: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user