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

+188
View File
@@ -0,0 +1,188 @@
package llm
import (
"context"
"errors"
"strings"
"testing"
)
// emptyStreamCases 是三家协议下"这一轮什么都没回来"的流形状。
//
// 关键不是"没收到终止事件"(有些兼容网关确实不发 [DONE],拿它当条件会把能用的
// 配置弄坏),而是**正文/工具调用/思考三样全空**。真实取证:一次 136 步的会话里
// 有 18 个这样的回合,全部 usage 缺失,每个都让用户多按一次「继续」。
func TestProvidersRejectEmptyStream(t *testing.T) {
cases := []struct {
name string
sse string
call func(url string) (Provider, error)
}{
{
name: "chat_completions:流直接关掉",
sse: "",
call: func(u string) (Provider, error) {
return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"}, nil
},
},
{
name: "chat_completions:只有 finish_reason 没有内容",
sse: "data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
call: func(u string) (Provider, error) {
return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"}, nil
},
},
{
name: "anthropic:只有 message_stop",
sse: "data: {\"type\":\"message_stop\"}\n\n",
call: func(u string) (Provider, error) {
return &AnthropicProvider{BaseURL: u, APIKey: "k", Model: "m", MaxTokens: 128}, nil
},
},
{
name: "responses:completed 但 output 为空",
sse: "data: {\"type\":\"response.completed\",\"response\":{\"output\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":0}}}\n\n",
call: func(u string) (Provider, error) {
return &ResponsesProvider{BaseURL: u, APIKey: "k", Model: "m"}, nil
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var body map[string]any
srv := captureServer(&body, c.sse)
defer srv.Close()
p, err := c.call(srv.URL)
if err != nil {
t.Fatal(err)
}
_, err = p.Stream(context.Background(), "sys",
[]Message{{Role: RoleUser, Content: "hi"}}, nil, StreamOptions{}, nil)
if !errors.Is(err, ErrEmptyCompletion) {
t.Fatalf("want ErrEmptyCompletion, got %v", err)
}
// 报错里要带协议名:三家都可能是那条坏流,排查时得知道是谁。
if !strings.Contains(err.Error(), "openai") && !strings.Contains(err.Error(), "anthropic") &&
!strings.Contains(err.Error(), "responses") {
t.Fatalf("错误里没有协议归因:%v", err)
}
})
}
}
// TestProvidersAcceptThinStreams:反过来也要钉住——只要有任何一样内容,就不算空。
// 尤其"只有思考没有正文"这一种,把模型的真实工作误判成失败会白白重试三次。
func TestProvidersAcceptThinStreams(t *testing.T) {
thin := []struct {
name string
sse string
call func(url string) Provider
}{
{"chat_completions 只回一句", "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\ndata: [DONE]\n\n",
func(u string) Provider { return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"} }},
{"chat_completions 只有工具调用", "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"c1\",\"function\":{\"name\":\"bash\",\"arguments\":\"{}\"}}]}}]}\n\ndata: [DONE]\n\n",
func(u string) Provider { return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"} }},
{"anthropic 只有 thinking", "data: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"T\"}}\n\ndata: {\"type\":\"message_stop\"}\n\n",
func(u string) Provider {
return &AnthropicProvider{BaseURL: u, APIKey: "k", Model: "m", MaxTokens: 128}
}},
}
for _, c := range thin {
t.Run(c.name, func(t *testing.T) {
var body map[string]any
srv := captureServer(&body, c.sse)
defer srv.Close()
if _, err := c.call(srv.URL).Stream(context.Background(), "sys", nil, nil, StreamOptions{}, nil); err != nil {
t.Fatalf("不该判成空回复:%v", err)
}
})
}
}
func TestIsEmptyCompletion(t *testing.T) {
cases := []struct {
name string
m Message
want bool
}{
{"全空", Message{}, true},
{"只有空白正文", Message{Content: " \n\t "}, true},
{"有正文", Message{Content: "答"}, false},
{"只有思考", Message{Reasoning: "想"}, false},
{"只有工具调用", Message{ToolCalls: []ToolCall{{ID: "1", Name: "bash"}}}, false},
// usage 不参与判定:观测到的坏流 usage 也缺失,但"有 usage 却整轮空"
// 同样不是一次有用的最终回答。
{"有 usage 但什么都没有", Message{Usage: &Usage{OutputTokens: 1}}, true},
}
for _, c := range cases {
if got := IsEmptyCompletion(c.m); got != c.want {
t.Errorf("%s: IsEmptyCompletion = %v, want %v", c.name, got, c.want)
}
}
}
// TestProvidersDetectTruncatedStream:真实取证那一例——62 秒只回一个词、usage 缺失、
// 没有终止事件。判据要求**两个证据都缺**,所以另外两种"缺一个"的合法形状必须放过:
// 有的网关不发 [DONE],有的不回 usage(我们确实请求了 include_usage,但兼容层不一定给)。
func TestProvidersDetectTruncatedStream(t *testing.T) {
cases := []struct {
name string
sse string
wantErr bool
provider func(url string) Provider
}{
{
name: "chat_completions:有正文,流静默关掉(实报的形状)",
sse: "data: {\"choices\":[{\"delta\":{\"content\":\"rawData\"}}]}\n\n",
wantErr: true,
provider: func(u string) Provider { return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"} },
},
{
name: "chat_completions:有正文 + [DONE],没 usage(兼容网关常态)",
sse: "data: {\"choices\":[{\"delta\":{\"content\":\"答\"}}]}\n\ndata: [DONE]\n\n",
wantErr: false,
provider: func(u string) Provider { return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"} },
},
{
name: "chat_completions:有正文 + usage,没 [DONE]",
sse: "data: {\"choices\":[{\"delta\":{\"content\":\"答\"}}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":2}}\n\n",
wantErr: false,
provider: func(u string) Provider { return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"} },
},
{
name: "chat_completions:只有 finish_reason 就断",
sse: "data: {\"choices\":[{\"delta\":{\"content\":\"答\"},\"finish_reason\":\"stop\"}]}\n\n",
wantErr: false, // finish_reason 本身就是终止证据
provider: func(u string) Provider { return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"} },
},
{
name: "anthropic:text 块开了却没等到 message_stop",
sse: "data: {\"type\":\"message_start\",\"message\":{\"id\":\"m\",\"role\":\"assistant\",\"content\":[],\"usage\":{\"input_tokens\":1}}}\n\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"半句\"}}\n\n",
wantErr: true,
provider: func(u string) Provider {
return &AnthropicProvider{BaseURL: u, APIKey: "k", Model: "m", MaxTokens: 128}
},
},
{
name: "responses:有增量却没有 response.completed",
sse: "data: {\"type\":\"response.output_text.delta\",\"delta\":\"半句\"}\n\n",
wantErr: true,
provider: func(u string) Provider { return &ResponsesProvider{BaseURL: u, APIKey: "k", Model: "m"} },
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var body map[string]any
srv := captureServer(&body, c.sse)
defer srv.Close()
_, err := c.provider(srv.URL).Stream(context.Background(), "sys",
[]Message{{Role: RoleUser, Content: "hi"}}, nil, StreamOptions{}, nil)
if got := err != nil; got != c.wantErr {
t.Fatalf("wantErr=%v,实际 err=%v", c.wantErr, err)
}
if c.wantErr && !errors.Is(err, ErrTruncatedStream) {
t.Fatalf("该报 ErrTruncatedStream,得到 %v", err)
}
})
}
}