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,175 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.gopher.ink/mirrors2/AutoZero/llm"
|
||||
"git.gopher.ink/mirrors2/AutoZero/session"
|
||||
"git.gopher.ink/mirrors2/AutoZero/tools"
|
||||
)
|
||||
|
||||
// TestModelCallTrace:每轮模型调用都应记录 StartedAt/DurationMs/Step。
|
||||
func TestModelCallTrace(t *testing.T) {
|
||||
prov := &scriptedProvider{fn: func(_ int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
|
||||
time.Sleep(6 * time.Millisecond)
|
||||
return llm.Message{Role: llm.RoleAssistant, Content: "done"}, nil
|
||||
}}
|
||||
a := &Agent{
|
||||
Provider: prov,
|
||||
Tools: tools.NewRegistry(),
|
||||
Session: &session.Session{},
|
||||
System: "sys",
|
||||
Opt: Options{Stdout: io.Discard, Stderr: io.Discard},
|
||||
}
|
||||
if err := a.Run(context.Background(), "hi"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msgs := a.Session.Messages
|
||||
last := msgs[len(msgs)-1]
|
||||
if last.Role != llm.RoleAssistant {
|
||||
t.Fatalf("last message should be assistant, got %s", last.Role)
|
||||
}
|
||||
if last.StartedAt == 0 {
|
||||
t.Error("assistant StartedAt not recorded")
|
||||
}
|
||||
if last.DurationMs < 5 {
|
||||
t.Errorf("assistant DurationMs = %d, want >= 5", last.DurationMs)
|
||||
}
|
||||
if last.Step != 1 {
|
||||
t.Errorf("assistant Step = %d, want 1", last.Step)
|
||||
}
|
||||
}
|
||||
|
||||
// slowTool 是一个可计时的假工具,delay 控制耗时。
|
||||
func slowTool(name string, ro, safe bool, delay time.Duration) *tools.Tool {
|
||||
return &tools.Tool{
|
||||
Name: name,
|
||||
Parameters: map[string]any{"type": "object"},
|
||||
ReadOnly: ro,
|
||||
ConcurrencySafe: safe,
|
||||
Run: func(_ context.Context, _ string) (tools.Result, error) {
|
||||
time.Sleep(delay)
|
||||
return tools.TextResult("ok"), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// toolMsgFor 取某次工具调用的回填消息。
|
||||
func toolMsgFor(sess *session.Session, id string) (llm.Message, bool) {
|
||||
for _, m := range sess.Messages {
|
||||
if m.Role == llm.RoleTool && m.ToolCallID == id {
|
||||
return m, true
|
||||
}
|
||||
}
|
||||
return llm.Message{}, false
|
||||
}
|
||||
|
||||
// TestToolCallTrace:工具耗时与所属轮次写进 tool 消息,且 done 事件带耗时。
|
||||
func TestToolCallTrace(t *testing.T) {
|
||||
a, sess := newTestAgent(t, slowTool("slow", false, false, 6*time.Millisecond))
|
||||
a.curStep = 3 // 模拟处于第 3 次模型调用
|
||||
|
||||
var events []ToolEvent
|
||||
a.Opt.OnTool = func(ev ToolEvent) { events = append(events, ev) }
|
||||
|
||||
if err := a.execTools(context.Background(), []llm.ToolCall{{ID: "c1", Name: "slow", Input: "{}"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
msg, ok := toolMsgFor(sess, "c1")
|
||||
if !ok {
|
||||
t.Fatal("tool result message not appended")
|
||||
}
|
||||
if msg.StartedAt == 0 || msg.DurationMs < 5 {
|
||||
t.Errorf("tool trace missing: %+v", msg)
|
||||
}
|
||||
if msg.Step != 3 {
|
||||
t.Errorf("tool Step = %d, want 3 (parent turn)", msg.Step)
|
||||
}
|
||||
|
||||
// done 事件应带耗时(供实时 UI 使用)。
|
||||
var done *ToolEvent
|
||||
for i := range events {
|
||||
if events[i].ID == "c1" && events[i].Done {
|
||||
done = &events[i]
|
||||
}
|
||||
}
|
||||
if done == nil {
|
||||
t.Fatal("no done event for c1")
|
||||
}
|
||||
if done.DurationMs < 5 || done.StartedAt == 0 {
|
||||
t.Errorf("done event missing trace: %+v", *done)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentToolTraceAttribution:并发批次的耗时按各自索引归属,不串号。
|
||||
func TestConcurrentToolTraceAttribution(t *testing.T) {
|
||||
a, sess := newTestAgent(t,
|
||||
slowTool("fast", true, true, 5*time.Millisecond),
|
||||
slowTool("slowish", true, true, 30*time.Millisecond),
|
||||
)
|
||||
calls := []llm.ToolCall{
|
||||
{ID: "c1", Name: "fast", Input: "{}"},
|
||||
{ID: "c2", Name: "slowish", Input: "{}"},
|
||||
}
|
||||
if err := a.execTools(context.Background(), calls); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fast, _ := toolMsgFor(sess, "c1")
|
||||
slowish, _ := toolMsgFor(sess, "c2")
|
||||
if fast.DurationMs <= 0 || slowish.DurationMs <= 0 {
|
||||
t.Fatalf("durations not recorded: fast=%d slowish=%d", fast.DurationMs, slowish.DurationMs)
|
||||
}
|
||||
if fast.DurationMs >= slowish.DurationMs {
|
||||
t.Errorf("durations attributed to the wrong call: fast=%d slowish=%d", fast.DurationMs, slowish.DurationMs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModelIdentityStamped:assistant 消息落盘要带「协议 + 模型」归因戳,
|
||||
// 缓存明细也得跟着落进会话文件;没实现 Identifier 的 provider(测试替身)
|
||||
// 必须留空而不是炸掉。
|
||||
func TestModelIdentityStamped(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
io.WriteString(w, "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\n")
|
||||
io.WriteString(w, "data: {\"choices\":[{\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":11,"+
|
||||
"\"completion_tokens\":2,\"prompt_tokens_details\":{\"cached_tokens\":9}}}\n\n")
|
||||
io.WriteString(w, "data: [DONE]\n\n")
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
a := &Agent{
|
||||
Provider: &llm.OpenAIProvider{BaseURL: srv.URL, APIKey: "k", Model: "gpt-test"},
|
||||
Tools: tools.NewRegistry(), Session: &session.Session{}, System: "sys",
|
||||
Opt: Options{Stdout: io.Discard, Stderr: io.Discard},
|
||||
}
|
||||
if err := a.Run(context.Background(), "hi"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
last := a.Session.Messages[len(a.Session.Messages)-1]
|
||||
if last.Format != llm.FormatChatCompletions || last.Model != "gpt-test" {
|
||||
t.Errorf("归因戳 = %q/%q, want chat_completions/gpt-test", last.Format, last.Model)
|
||||
}
|
||||
if last.Usage == nil || last.Usage.CacheReadTokens != 9 || last.Usage.InputTokens != 11 {
|
||||
t.Errorf("明细没落到会话:%+v", last.Usage)
|
||||
}
|
||||
|
||||
prov := &scriptedProvider{fn: func(_ int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
|
||||
return llm.Message{Role: llm.RoleAssistant, Content: "x"}, nil
|
||||
}}
|
||||
b := &Agent{Provider: prov, Tools: tools.NewRegistry(), Session: &session.Session{}, System: "sys",
|
||||
Opt: Options{Stdout: io.Discard, Stderr: io.Discard}}
|
||||
if err := b.Run(context.Background(), "hi"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
end := b.Session.Messages[len(b.Session.Messages)-1]
|
||||
if end.Format != "" || end.Model != "" {
|
||||
t.Errorf("未实现 Identifier 时不该有戳:%q/%q", end.Format, end.Model)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user