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

+354
View File
@@ -0,0 +1,354 @@
package llm
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
// ResponsesProvider 走 OpenAI Responses API(/responses,SSE 流式)。
// 与 Chat Completions 的差异:事件驱动流(response.output_text.delta 等)、
// 工具调用经 function_call/function_call_output 消息往返、图片用 input_image。
type ResponsesProvider struct {
BaseURL string
APIKey string
Model string
HTTP *http.Client
}
// SetModel 运行时切换模型(ModelSwitcher)。
func (p *ResponsesProvider) SetModel(model string) {
p.Model = model
}
// CurrentModel 报告当前模型(CurrentModeler)。
func (p *ResponsesProvider) CurrentModel() string { return p.Model }
// Identity 自报协议与当前模型(Identifier)。
func (p *ResponsesProvider) Identity() (string, string) { return FormatResponses, p.Model }
// ListModels 枚举网关可用模型(ModelLister),与 Chat Completions 同一 GET /models。
func (p *ResponsesProvider) ListModels(ctx context.Context) ([]string, error) {
op := &OpenAIProvider{BaseURL: p.BaseURL, APIKey: p.APIKey, HTTP: p.HTTP}
return op.ListModels(ctx)
}
// rsEvent 是 /responses SSE 事件包络(外层只取 type,其余按事件类型二次解码)。
type rsEvent struct {
Type string `json:"type"`
}
// rsItemEvent 载荷:response.output_item.added / .done 的 function_call 项。
type rsItemEvent struct {
Item *rsOutputItem `json:"item"`
}
// rsArgsDeltaEvent 载荷:response.function_call_arguments.delta。
type rsArgsDeltaEvent struct {
ItemID string `json:"item_id"`
CallID string `json:"call_id"`
Arguments string `json:"delta"`
}
// rsCompletedEvent 载荷:response.completed / response.incomplete。
type rsCompletedEvent struct {
Response struct {
Output []rsOutputItem `json:"output"`
Usage rsUsageWrap `json:"usage"`
// Status 是 completed / incomplete;incomplete 时必须看 IncompleteDetails
// 才知道是不是"输出上限截住"(另一种是内容审核拦下)。
Status string `json:"status"`
IncompleteDetails struct {
Reason string `json:"reason"`
} `json:"incomplete_details"`
} `json:"response"`
}
type rsOutputItem struct {
Type string `json:"type"`
ID string `json:"id"`
CallID string `json:"call_id"`
Name string `json:"name"`
Arguments string `json:"arguments"`
}
type rsUsageWrap struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
// 明细:input_tokens 已含缓存命中部分(与 Chat Completions 同口径)。
InputDetails struct {
Cached int `json:"cached_tokens"`
} `json:"input_tokens_details"`
OutputDetails struct {
Reasoning int `json:"reasoning_tokens"`
} `json:"output_tokens_details"`
}
func (p *ResponsesProvider) Stream(ctx context.Context, system string, msgs []Message, tools []ToolDef, opts StreamOptions, onDelta func(StreamDelta)) (Message, error) {
body := map[string]any{
"model": p.Model,
"input": rsBuildInput(joinSystem(system, opts.SystemDynamic), msgs),
"stream": true,
"store": false,
"max_output_tokens": 8192,
}
if len(tools) > 0 {
body["tools"] = rsBuildTools(tools)
if opts.DisableTools {
body["tool_choice"] = "none"
}
}
// 思考级别:仅开启时发送;推理 token 计入 max_output_tokens,故一并抬高上限防截断。
if opts.Thinking.Enabled() {
body["reasoning"] = map[string]any{"effort": string(opts.Thinking)}
body["max_output_tokens"] = 16384
}
payload, err := json.Marshal(body)
if err != nil {
return Message{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
strings.TrimRight(p.BaseURL, "/")+"/responses", bytes.NewReader(payload))
if err != nil {
return Message{}, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+p.APIKey)
resp, err := doWithRetry(ctx, p.http(), req, payload)
if err != nil {
return Message{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var buf bytes.Buffer
_, _ = buf.ReadFrom(io.LimitReader(resp.Body, 4096))
return Message{}, fmt.Errorf("responses: HTTP %d: %s", resp.StatusCode, buf.String())
}
final := Message{Role: RoleAssistant}
var st streamState
// callID -> 增量参数缓冲(function_call_arguments.delta 事件流)。
argBuf := map[string]*ToolCall{}
// itemID -> callID(function_call_arguments 事件只带 item_id/或 call_id,二者取其一)。
itemToCall := map[string]string{}
emitted := map[string]bool{}
emitCall := func(tc ToolCall) {
if onDelta == nil || tc.ID == "" || emitted[tc.ID] {
return
}
emitted[tc.ID] = true
c := tc
onDelta(StreamDelta{ToolCall: &c})
}
sg := guardStream(resp.Body, streamIdleTimeout)
defer sg.Stop()
sc := bufio.NewScanner(sg)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
raw := []byte(strings.TrimPrefix(line, "data: "))
var env rsEvent
if err := json.Unmarshal(raw, &env); err != nil {
continue
}
switch env.Type {
case "response.output_item.added", "response.output_item.done":
var ev rsItemEvent
if err := json.Unmarshal(raw, &ev); err != nil || ev.Item == nil || ev.Item.Type != "function_call" {
continue
}
// added 通常只带名字,done 带完整参数;参数为空时保留增量缓冲里已收到的那份。
tc := argBuf[ev.Item.CallID]
if tc == nil {
tc = &ToolCall{ID: ev.Item.CallID}
argBuf[ev.Item.CallID] = tc
}
if ev.Item.Name != "" {
tc.Name = ev.Item.Name
}
if ev.Item.Arguments != "" {
tc.Input = ev.Item.Arguments
}
if ev.Item.ID != "" {
itemToCall[ev.Item.ID] = ev.Item.CallID
}
// done 事件的 parameters 已完整:提前通知,只读工具可先跑。
if env.Type == "response.output_item.done" {
emitCall(*tc)
}
case "response.function_call_arguments.delta":
var ev rsArgsDeltaEvent
if err := json.Unmarshal(raw, &ev); err != nil {
continue
}
callID := ev.CallID
if callID == "" {
callID = itemToCall[ev.ItemID] // 某些网关只带 item_id
}
if tc := argBuf[callID]; tc != nil {
tc.Input += ev.Arguments
}
// 思考内容:summary(多网关可见)与完整推理文本都收。
case "response.reasoning_summary_text.delta", "response.reasoning_text.delta":
var ev struct {
Delta string `json:"delta"`
}
if err := json.Unmarshal(raw, &ev); err != nil || ev.Delta == "" {
continue
}
final.Reasoning += ev.Delta
if onDelta != nil {
onDelta(StreamDelta{Reasoning: ev.Delta})
}
case "response.output_text.delta":
var ev struct {
Delta string `json:"delta"`
}
if err := json.Unmarshal(raw, &ev); err != nil || ev.Delta == "" {
continue
}
final.Content += ev.Delta
if onDelta != nil {
onDelta(StreamDelta{Text: ev.Delta})
}
case "response.completed", "response.incomplete":
var ev rsCompletedEvent
if err := json.Unmarshal(raw, &ev); err == nil {
final.Usage = &Usage{
InputTokens: ev.Response.Usage.InputTokens,
OutputTokens: ev.Response.Usage.OutputTokens,
CacheReadTokens: ev.Response.Usage.InputDetails.Cached,
ReasoningTokens: ev.Response.Usage.OutputDetails.Reasoning,
}
// 以 completed 事件的权威 output 为准重建 tool calls(增量网关兜底)。
final.ToolCalls = nil
for _, it := range ev.Response.Output {
if it.Type == "function_call" && it.Name != "" {
tc := ToolCall{ID: it.CallID, Name: it.Name, Input: it.Arguments}
final.ToolCalls = append(final.ToolCalls, tc)
emitCall(tc)
}
}
// incomplete 有两种:输出上限截住(可续写)与内容审核拦下(续写无意义)。
if ev.Response.Status == "incomplete" && marksOutputLimit(ev.Response.IncompleteDetails.Reason) {
final.OutputLimited = true
}
}
st.doneSeen = true
return completion("responses", final, st)
}
}
if err := sc.Err(); err != nil {
if stall := sg.Err(); stall != nil {
return Message{}, fmt.Errorf("responses: %w", stall)
}
return Message{}, fmt.Errorf("responses: stream: %w", err)
}
// 看门狗掐断的流不走"尽力拼装":那会把半截输出当成一轮完整回复。
if stall := sg.Err(); stall != nil {
return Message{}, fmt.Errorf("responses: %w", stall)
}
// 无 response.completed(连接截断):用增量缓冲尽力拼装。
for _, tc := range argBuf {
if tc.Name != "" {
final.ToolCalls = append(final.ToolCalls, *tc)
emitCall(*tc)
}
}
return completion("responses", final, st)
}
// rsBuildInput 构造 /responses 的 input 数组:system 作为首条 system 消息,
// tool 结果映射为 function_call_output,assistant 工具调用拆成 function_call 条目。
func rsBuildInput(system string, msgs []Message) []map[string]any {
out := []map[string]any{}
if system != "" {
out = append(out, map[string]any{"role": "system", "content": system})
}
for _, m := range msgs {
switch {
case m.Role == RoleTool:
out = append(out, map[string]any{
"type": "function_call_output",
"call_id": m.ToolCallID,
"output": m.Content,
})
if len(m.Images) > 0 {
// function_call_output 不收图片:附一条 user 消息带图。
out = append(out, map[string]any{
"role": "user",
"content": rsContentParts("[image(s) attached from tool result]", m.Images),
})
}
case len(m.ToolCalls) > 0:
// assistant 的 function_call 拆成独立条目;文本部分单独一条。
if m.Content != "" {
out = append(out, map[string]any{"role": "assistant", "content": m.Content})
}
for _, tc := range m.ToolCalls {
out = append(out, map[string]any{
"type": "function_call",
"call_id": tc.ID,
"name": tc.Name,
"arguments": tc.Input,
})
}
case len(m.Images) > 0:
out = append(out, map[string]any{"role": string(m.Role), "content": rsContentParts(m.Content, m.Images)})
default:
out = append(out, map[string]any{"role": string(m.Role), "content": m.Content})
}
}
return out
}
// rsContentParts 构造多模态 content(text + input_image)。
func rsContentParts(text string, imgs []Image) []map[string]any {
parts := []map[string]any{}
if text != "" {
parts = append(parts, map[string]any{"type": "input_text", "text": text})
}
for _, img := range imgs {
b64, mt, err := loadImageRef(img)
if err != nil {
parts = append(parts, map[string]any{"type": "input_text", "text": imageUnavailable(img, err)})
continue
}
parts = append(parts, map[string]any{
"type": "input_image",
"image_url": "data:" + mt + ";base64," + b64,
})
}
return parts
}
func rsBuildTools(tools []ToolDef) []map[string]any {
out := make([]map[string]any, 0, len(tools))
for _, t := range tools {
out = append(out, map[string]any{
"type": "function",
"name": t.Name,
"description": t.Description,
"parameters": t.Parameters,
})
}
return out
}
func (p *ResponsesProvider) http() *http.Client {
if p.HTTP != nil {
return p.HTTP
}
return defaultHTTPClient
}