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

+128
View File
@@ -0,0 +1,128 @@
package llm
import (
"io"
"net"
"net/http"
"sync"
"time"
)
// streamIdleTimeout 是流式响应允许的最长静默。
//
// 为什么需要它:三家 provider 都是 `bufio.Scanner` 死读 resp.Body,而 ctx 一路
// 传下去却**没有任何超时**(客户端也是无超时的 http.DefaultClient)。上游"连上了
// 但不吐字节"(网关半开连接、服务端卡在生成阶段)时,Read 永久阻塞——整个回合
// 就此静止,用户既等不到结果也看不出出了什么。这是第二类"卡死",且比 bash 那条
// 更容易在长任务里撞上(长任务 = 长思考 = 长静默)。
//
// 90 秒是权衡后的值:推理模型的首 token 延迟与中途长思考都会静默,几十秒属正常,
// 而真正卡死的连接通常连几十秒都不返回。宁可宽一点,也不要错杀慢但活的网关。
// var 仅供测试缩短。
var streamIdleTimeout = 90 * time.Second
// defaultHTTPClient 是 provider 没显式给 HTTP 客户端时的默认值。
//
// 不用 http.DefaultClient:① 它是进程级全局,改它会影响别人的代码,不改它就没有
// 任何超时;② 这里要的是"首字节前有限等待",而 Client.Timeout 覆盖**整个响应体**,
// 设了就会把正常的长流式掐死。所以只设 ResponseHeaderTimeout + dial/TLS 超时,
// 读流阶段的看门狗交给 idleGuard。
var defaultHTTPClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 90 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConnsPerHost: 8,
},
}
// idleGuard 是包在 SSE 响应体外面的静默看门狗:每次读到字节就重置计时,
// 静默超阈值就关掉连接,从而解阻塞正在 Read 的那个 goroutine。
type idleGuard struct {
body io.ReadCloser
idle time.Duration
mu sync.Mutex
last time.Time
once sync.Once
stalled bool
done chan struct{}
}
// guardStream 开始看护 body,返回可直接交给 bufio.Scanner 的 io.Reader。
// 调用方必须 defer Stop(),否则看护 goroutine 会活到进程结束。
func guardStream(body io.ReadCloser, idle time.Duration) *idleGuard {
g := &idleGuard{body: body, idle: idle, last: time.Now(), done: make(chan struct{})}
go g.watch()
return g
}
func (g *idleGuard) Read(p []byte) (int, error) {
n, err := g.body.Read(p)
if n > 0 {
g.mu.Lock()
g.last = time.Now()
g.mu.Unlock()
}
return n, err
}
func (g *idleGuard) Close() error { return g.body.Close() }
// watch 周期检查静默时长。阈值本身可能很长(90s),tick 取 idle/4:
// 既不会让正常流多出可观开销,也能在半分钟内发现"再也不会有数据"的连接。
func (g *idleGuard) watch() {
tick := g.idle / 4
if tick < 20*time.Millisecond {
tick = 20 * time.Millisecond // 测试里 idle 会被缩到毫秒级
}
t := time.NewTicker(tick)
defer t.Stop()
for {
select {
case <-g.done:
return
case <-t.C:
g.mu.Lock()
quiet := time.Since(g.last)
g.mu.Unlock()
if quiet > g.idle {
g.once.Do(func() {
g.mu.Lock()
g.stalled = true
g.mu.Unlock()
// 唯一的解阻塞手段:Read 卡在传输层时没有任何回调可指望。
_ = g.body.Close()
})
return
}
}
}
}
// Stop 结束看护(流正常读完后调用)。
func (g *idleGuard) Stop() { close(g.done) }
// Err 报告这次流是不是被看门狗掐断的。给上层的意义是"能说出真实原因":
// 否则调用方只会看到一句来历不明的 "read tcp: use of closed connection"。
func (g *idleGuard) Err() error {
g.mu.Lock()
defer g.mu.Unlock()
if g.stalled {
return &StalledError{Idle: g.idle}
}
return nil
}
// StalledError 是"服务端长时间不吐字节"这一类失败。可被上层 errors.As 识别。
type StalledError struct{ Idle time.Duration }
func (e *StalledError) Error() string {
return "provider stream stalled: no data for " + e.Idle.String()
}