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,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()
|
||||
}
|
||||
Reference in new issue
Block a user