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

+80
View File
@@ -0,0 +1,80 @@
package agent
import (
"strings"
"testing"
"unicode/utf8"
"git.gopher.ink/mirrors2/AutoZero/llm"
)
// 截断类代码的共性坑:`s[:n]` 的 n 是**字节**下标,而这里的文本大量是中文(每字 3 字节)
// 与 emoji(4 字节)。切在中间不会报错,只会留下一个非法字节序列——发给模型是乱码,
// 某些网关直接 400,写进会话 JSONL 后连回读都不干净。
// 这几条用例的共同断言是 utf8.ValidString:它们在修复前全部会失败(预算恰好落在半个字上)。
const cjkLine = "把中文标题与 emoji 😀 都留下,别劈开" // 每轮 3 字节为主,含一个 4 字节 emoji
func TestPreviewKeepsUTF8(t *testing.T) {
// 逐个预算长度试:只要有一个切点落在字符中间,preview 就会产出非法序列。
long := strings.Repeat(cjkLine, 30)
for n := 1; n <= utf8.RuneCountInString(long); n++ {
// preview 的预算固定 60 字,所以这里改的是内容里"第 60 个字符落在哪"。
got := preview(strings.Repeat("中", n) + " " + long)
if !utf8.ValidString(got) {
t.Fatalf("preview 在偏移 %d 处劈开字符:%q", n, got)
}
}
}
func TestCompactKeepsUTF8(t *testing.T) {
// 工具参数里的中文路径/内容是常态(write 的 content、bash 的 echo)。
input := `{"path":"` + strings.Repeat("中文路径", 60) + `","content":"` + strings.Repeat("内容", 200) + `"}`
if got := compact(input); !utf8.ValidString(got) {
t.Fatalf("compact 产出非法 UTF-8:%q", got)
}
// 非 JSON 入参走原样截断那条路,同样不能切坏。
raw := strings.Repeat("查询", 300)
if got := compact(raw); !utf8.ValidString(got) {
t.Fatalf("compact 对非 JSON 入参产出非法 UTF-8:%q", got)
}
}
// TestSerializeForSummaryKeepsUTF8 是最贵的一处:这条串就是发给模型的摘要提示词,
// 而 perMsgCap 是字节预算,中文消息下几乎必然切在半个字上。
func TestSerializeForSummaryKeepsUTF8(t *testing.T) {
msgs := []llm.Message{
{Role: llm.RoleUser, Content: strings.Repeat("用户诉求", 200)},
{Role: llm.RoleAssistant, Content: strings.Repeat("回答内容", 200)},
{Role: llm.RoleAssistant, Content: "先看一下", ToolCalls: []llm.ToolCall{
{ID: "c1", Name: "bash", Input: `{"command":"echo ` + strings.Repeat("输出中文", 500) + `"}`},
}},
{Role: llm.RoleTool, ToolCallID: "c1", Content: strings.Repeat("工具结果", 400)},
}
for _, cap := range []int{1, 5, 7, 64, 200, 1999, 2001} {
got := serializeForSummary(msgs, cap, 0)
if !utf8.ValidString(got) {
t.Errorf("perMsgCap=%d 时摘要输入含非法字符", cap)
}
}
}
// TestTruncateToolResultsStillAligns 是既有正确实现的护栏(这两处本来就退到字符边界):
// 它们不该被后来的"统一截断"重构换掉语义——头尾都要能独立成为一个合法串。
func TestTruncateToolResultsStillAligns(t *testing.T) {
content := strings.Repeat("中文输出", 5000) + "😀"
out := truncateToolResults([]llm.Message{{Role: llm.RoleTool, ToolCallID: "1", Content: content}}, 1000)
if !utf8.ValidString(out[0].Content) {
t.Errorf("截断后的工具结果非法:%q", out[0].Content[len(out[0].Content)-40:])
}
// 预算放不下一个字符时要给空串,而不是半个;放得下时就给整字。
if got := cutUTF8Head(content, 2); got != "" {
t.Errorf("2 字节预算放不下一个中文,应得空串,得到 %q", got)
}
if got := cutUTF8Head(content, 4); got != "中" {
t.Errorf("4 字节预算应正好装下第一个中文,得到 %q", got)
}
if !utf8.ValidString(cutUTF8Tail(content, 5)) {
t.Error("cutUTF8Tail 切坏了")
}
}