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

+114
View File
@@ -0,0 +1,114 @@
package llm
import (
"errors"
"fmt"
"strings"
)
// 空补全:一次模型调用回来,**正文、工具调用、思考内容三样全没有**。
//
// 为什么单独立一个错误:网关把 SSE 流"干净地关掉"(不发 [DONE]/message_stop,
// 也不触发静默看门狗)时,provider 会返回一条空 Message + nil error,而 agent
// 的收尾判定是"这条没有 tool_calls ⇒ 模型说完了"——于是回合被当成**成功结束**,
// 界面一个字都不出。用户看到的症状是"干不了几步就停,得手动按继续"。
// 真实取证:20261009-213751 一次 136 步的会话里有 18 个这样的空回合
// (全部 usage 缺失、中位 3.0s、12 个紧跟在工具结果之后),对应 23 次人工「继续」。
//
// 判定刻意**不依赖终止事件**:有些兼容网关确实不发 [DONE],拿"没收到终止事件"
// 当条件是会把能用的配置弄坏的。"整轮什么都没有"则是无歧义的——一个 agent 回合
// 里什么都没有,永远不是一次有用的最终回答。
// ErrEmptyCompletion 是"这一轮什么都没返回"。可被 errors.Is 识别,上层据此重试。
var ErrEmptyCompletion = errors.New("模型返回空回复(没有正文、没有工具调用、没有思考内容)")
// IsEmptyCompletion 判断一次补全是否什么都没有。usage 不参与判定——
// 观测到的坏流量 usage 也缺失,但"有 usage 却整轮空"同样不该被当成最终回答。
func IsEmptyCompletion(m Message) bool {
return strings.TrimSpace(m.Content) == "" &&
strings.TrimSpace(m.Reasoning) == "" &&
len(m.ToolCalls) == 0
}
// ErrTruncatedStream 是"流被掐断":既没收到任何终止事件,也没拿到 usage。
//
// 为什么两种信号都要缺才算:网关不发 [DONE] 的情况真实存在(拿"没收到终止事件"
// 单独当条件会把这些配置弄坏),而有些网关确实不回 usage。两个都没有才算证据——
// 一次正常的补全至少留下其中一个。
//
// 咬人的样子(会话 20261010-092121 实测):一次 62 秒的调用只吐出 "rawData" 一个词、
// usage 缺失、没有工具调用,而全库 179 条 assistant 只有这一条 usage 缺失。它被
// "没有 tool_calls ⇒ 模型说完了"当成正常收尾,用户只能打「继续」。
var ErrTruncatedStream = errors.New("模型回复被截断(流里既没有终止事件也没有 usage)")
// streamState 是一次流式调用的收尾证据,由各家 provider 在解析循环里填。
type streamState struct {
usageSeen bool // 收到过 usage 块
doneSeen bool // 收到 [DONE] / message_stop / response.completed / finish_reason
}
// outputLimitReasons 是三家里"输出被 token 上限切断"的终止原因。
// 各家叫法不同(Chat Completions 用 length,Anthropic 用 max_tokens,Responses 用
// max_output_tokens),归成一个信号给上层,别让 agent 认三家方言。
//
// 刻意**不含** model_context_window_exceeded:那是上下文满了,续写只会连着撞同一堵墙,
// 该走的是压缩(agent/context.go),不是"接着写"。
var outputLimitReasons = map[string]bool{
"length": true,
"max_tokens": true,
"max_output_tokens": true,
}
// IsTransientStream 报告一次失败属于"这条请求没走完 / 网关没答上"——重发同一个请求
// 就有机会拿到完整回复。与之相对的是**确定性**失败(HTTP 400 说请求本身不合法、鉴权
// 失败、上下文满),重发只会再撞同一堵墙,所以那些不该重试。
//
// 三类证据:整轮空、流被截断、流卡死到空闲看门狗上限;再加一组传输层措辞(连接被掐、
// 半路 EOF、网关 429/5xx)。它们都发生在流已开始之后,llm 那层的前置重试
// (doWithRetry 只覆盖"响应开始前")管不到,只能由上层按请求重发。
func IsTransientStream(err error) bool {
if err == nil {
return false
}
if errors.Is(err, ErrEmptyCompletion) || errors.Is(err, ErrTruncatedStream) {
return true
}
var stalled *StalledError
if errors.As(err, &stalled) {
return true
}
msg := strings.ToLower(err.Error())
for _, mark := range transientErrorMarks {
if strings.Contains(msg, mark) {
return true
}
}
return false
}
// transientErrorMarks 是传输层失败的措辞。刻意不含 cancel:人被叫停时不该再发请求。
var transientErrorMarks = []string{
"connection reset", "connection refused", "broken pipe", "i/o timeout",
"eof", "too many requests", "http 429", "http 500", "http 502", "http 503", "http 504",
}
func marksOutputLimit(reason string) bool {
return outputLimitReasons[strings.ToLower(strings.TrimSpace(reason))]
}
// completion 是三家 provider 的统一出口:空回复与截断流都按失败返回。
// prefix 带上协议名,报错时能看出是哪一路 provider 撞的。
func completion(prefix string, m Message, st streamState) (Message, error) {
if IsEmptyCompletion(m) {
return Message{}, fmt.Errorf("%s: %w", prefix, ErrEmptyCompletion)
}
if !st.usageSeen && !st.doneSeen {
return Message{}, fmt.Errorf("%s: %w", prefix, ErrTruncatedStream)
}
// 带工具调用的响应不算"被切断的最终回答":那时 finish_reason 通常是 tool_calls,
// 而真是 length 也说明参数 JSON 已经残缺——续写救不回来,该重发这一步(另一件事)。
if len(m.ToolCalls) > 0 {
m.OutputLimited = false
}
return m, nil
}