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,129 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 输出上限切断的识别:三家的叫法不同,归一成 Message.OutputLimited 一个信号。
|
||||
// 每个 fixture 都带正文——"什么都没有"是空回复那条路(ErrEmptyCompletion),
|
||||
// 这里要抓的是"合法、有内容、但没写完"。
|
||||
func TestProvidersDetectOutputLimit(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
sse string
|
||||
call func(url string) (Provider, error)
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "chat_completions:finish_reason=length",
|
||||
sse: "data: {\"choices\":[{\"delta\":{\"content\":\"第一段写完了,第二段\"}}]}\n\n" +
|
||||
"data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"length\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7}}\n\n" +
|
||||
"data: [DONE]\n\n",
|
||||
call: func(u string) (Provider, error) { return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"}, nil },
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "chat_completions:finish_reason=stop 是正常收尾",
|
||||
sse: "data: {\"choices\":[{\"delta\":{\"content\":\"说完了\"}}]}\n\n" +
|
||||
"data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":3}}\n\n" +
|
||||
"data: [DONE]\n\n",
|
||||
call: func(u string) (Provider, error) { return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"}, nil },
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
// 上下文满 ≠ 输出被切断:续写只会连着撞同一堵墙,该走压缩。
|
||||
name: "chat_completions:context window 不算输出切断",
|
||||
sse: "data: {\"choices\":[{\"delta\":{\"content\":\"半截\"}}]}\n\n" +
|
||||
"data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"model_context_window_exceeded\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":2}}\n\n" +
|
||||
"data: [DONE]\n\n",
|
||||
call: func(u string) (Provider, error) { return &OpenAIProvider{BaseURL: u, APIKey: "k", Model: "m"}, nil },
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "anthropic:message_delta 的 stop_reason=max_tokens",
|
||||
sse: "data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n" +
|
||||
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"还在写\"}}\n\n" +
|
||||
"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"max_tokens\"},\"usage\":{\"output_tokens\":64}}\n\n" +
|
||||
"data: {\"type\":\"message_stop\"}\n\n",
|
||||
call: func(u string) (Provider, error) {
|
||||
return &AnthropicProvider{BaseURL: u, APIKey: "k", Model: "m", MaxTokens: 128}, nil
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "responses:incomplete + max_output_tokens",
|
||||
sse: "data: {\"type\":\"response.output_text.delta\",\"delta\":\"写了一半\"}\n\n" +
|
||||
"data: {\"type\":\"response.incomplete\",\"response\":{\"status\":\"incomplete\",\"output\":[],\"incomplete_details\":{\"reason\":\"max_output_tokens\"},\"usage\":{\"input_tokens\":4,\"output_tokens\":9}}}\n\n",
|
||||
call: func(u string) (Provider, error) { return &ResponsesProvider{BaseURL: u, APIKey: "k", Model: "m"}, nil },
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// 审核拦下的 incomplete 续写无意义(模型不是没写完,是不肯写完)。
|
||||
name: "responses:incomplete 但原因是内容审核",
|
||||
sse: "data: {\"type\":\"response.output_text.delta\",\"delta\":\"抱歉\"}\n\n" +
|
||||
"data: {\"type\":\"response.incomplete\",\"response\":{\"status\":\"incomplete\",\"output\":[],\"incomplete_details\":{\"reason\":\"content_filter\"},\"usage\":{\"input_tokens\":4,\"output_tokens\":2}}}\n\n",
|
||||
call: func(u string) (Provider, error) { return &ResponsesProvider{BaseURL: u, APIKey: "k", Model: "m"}, nil },
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
m, err := p.Stream(context.Background(), "sys",
|
||||
[]Message{{Role: RoleUser, Content: "hi"}}, nil, StreamOptions{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("这一轮有正文,不该报错:%v", err)
|
||||
}
|
||||
if m.OutputLimited != c.want {
|
||||
t.Errorf("OutputLimited = %v,want %v(正文 %q)", m.OutputLimited, c.want, m.Content)
|
||||
}
|
||||
if strings.TrimSpace(m.Content) == "" {
|
||||
t.Error("fixture 没把正文收进来,用例本身失效")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestToolCallAtOutputLimitIsNotMarkedCut:带工具调用的响应不算"被切断的最终回答"。
|
||||
// 真是 length 也说明参数 JSON 已经残缺,续写救不回来——标了反而让上层去续一个
|
||||
// 不该续的东西。
|
||||
func TestToolCallAtOutputLimitIsNotMarkedCut(t *testing.T) {
|
||||
sse := "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"c1\",\"type\":\"function\"," +
|
||||
"\"function\":{\"name\":\"bash\",\"arguments\":\"{\\\"command\\\":\\\"ls\\\"}\"}}]}}]}\n\n" +
|
||||
"data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"length\"}],\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":4}}\n\n" +
|
||||
"data: [DONE]\n\n"
|
||||
var body map[string]any
|
||||
srv := captureServer(&body, sse)
|
||||
defer srv.Close()
|
||||
p := &OpenAIProvider{BaseURL: srv.URL, APIKey: "k", Model: "m"}
|
||||
m, err := p.Stream(context.Background(), "sys", []Message{{Role: RoleUser, Content: "hi"}}, nil, StreamOptions{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(m.ToolCalls) != 1 {
|
||||
t.Fatalf("工具调用没收到:%+v", m.ToolCalls)
|
||||
}
|
||||
if m.OutputLimited {
|
||||
t.Error("带 tool_calls 的响应被标成了输出切断")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarksOutputLimit(t *testing.T) {
|
||||
for r, want := range map[string]bool{
|
||||
"length": true, "LENGTH": true, " max_tokens": true, "max_output_tokens": true,
|
||||
"stop": false, "tool_calls": false, "": false, "model_context_window_exceeded": false,
|
||||
"content_filter": false,
|
||||
} {
|
||||
if got := marksOutputLimit(r); got != want {
|
||||
t.Errorf("marksOutputLimit(%q) = %v,want %v", r, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user