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
+216
@@ -0,0 +1,216 @@
|
||||
// Package llm 定义统一的 LLM Provider 抽象(借鉴 pigo internal/provider 的
|
||||
// 统一层思路),当前支持 OpenAI 兼容与 Anthropic 两种协议,均为手写 SSE 流式。
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Role 取值:system / user / assistant / tool。
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleSystem Role = "system"
|
||||
RoleUser Role = "user"
|
||||
RoleAssistant Role = "assistant"
|
||||
RoleTool Role = "tool"
|
||||
)
|
||||
|
||||
// ThinkingLevel 是每轮请求的思考/推理深度。注意零值是 ""(不是 ThinkingOff)。
|
||||
type ThinkingLevel string
|
||||
|
||||
const (
|
||||
ThinkingOff ThinkingLevel = "off"
|
||||
ThinkingLow ThinkingLevel = "low"
|
||||
ThinkingMedium ThinkingLevel = "medium"
|
||||
ThinkingHigh ThinkingLevel = "high"
|
||||
)
|
||||
|
||||
// Enabled 仅对 low/medium/high 为真;off 与零值 "" 均为 false。
|
||||
// 判定务必用本方法,不要写 `!= ThinkingOff`(零值不等于 off)。
|
||||
func (l ThinkingLevel) Enabled() bool {
|
||||
return l == ThinkingLow || l == ThinkingMedium || l == ThinkingHigh
|
||||
}
|
||||
|
||||
// ParseThinkingLevel 归一化输入;空值与未知值一律回退 ThinkingOff。
|
||||
func ParseThinkingLevel(s string) ThinkingLevel {
|
||||
switch ThinkingLevel(strings.ToLower(strings.TrimSpace(s))) {
|
||||
case ThinkingLow:
|
||||
return ThinkingLow
|
||||
case ThinkingMedium:
|
||||
return ThinkingMedium
|
||||
case ThinkingHigh:
|
||||
return ThinkingHigh
|
||||
default:
|
||||
return ThinkingOff
|
||||
}
|
||||
}
|
||||
|
||||
// IsThinkingLevel 报告 s 是否为四个合法值之一(配置校验用)。
|
||||
func IsThinkingLevel(s string) bool {
|
||||
v := ThinkingLevel(strings.ToLower(strings.TrimSpace(s)))
|
||||
return v == ThinkingOff || v.Enabled()
|
||||
}
|
||||
|
||||
// StreamOptions 是单轮请求的可选参数。零值 = off:不发送任何新字段。
|
||||
type StreamOptions struct {
|
||||
Thinking ThinkingLevel
|
||||
// SystemDynamic 是不该进入 prompt-cache 断点的 system 后缀:计划、记忆、
|
||||
// 回退分支摘要这类每轮可能变化的内容。Anthropic 把它作为第二个 system 块
|
||||
// (第一个块带 cache_control);OpenAI 兼容/Responses 追加在同一条 system
|
||||
// 消息尾部——token 前缀缓存仍命中稳定段,同时避免部分网关不接受多条
|
||||
// system 消息。
|
||||
SystemDynamic string
|
||||
// DisableTools 保留 tools 定义、但禁止模型调用(三家的 tool_choice=none)。
|
||||
// 收尾回合用它而不是把 tools 传成 nil:换掉工具数组会打碎缓存前缀。
|
||||
DisableTools bool
|
||||
}
|
||||
|
||||
// ToolCall 是模型发起的一次工具调用请求。
|
||||
type ToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input string `json:"input"` // 原始 JSON 参数字符串
|
||||
}
|
||||
|
||||
// Image 是随消息发送的图片(按文件名引用,构建请求时从缓存读取并 base64 编码;
|
||||
// 会话 JSONL 只存文件名,不存像素)。
|
||||
type Image struct {
|
||||
Filename string `json:"filename"` // 缓存中的文件名
|
||||
Path string `json:"path,omitempty"` // 兼容旧格式:直接路径
|
||||
}
|
||||
|
||||
// Message 是会话中的一条消息。ToolCallID 非空表示这是工具结果回填。
|
||||
type Message struct {
|
||||
Role Role `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
Images []Image `json:"images,omitempty"` // 常见于截图类工具结果
|
||||
Usage *Usage `json:"usage,omitempty"`
|
||||
// Reasoning 是开启思考时的推理文本(展示用;Anthropic 场景还需回传,见下)。
|
||||
Reasoning string `json:"reasoning,omitempty"`
|
||||
// ReasoningSignature 是 Anthropic thinking 块的签名:工具调用场景下,
|
||||
// 下一轮请求必须原样回传 thinking 块(含签名),否则 API 会拒绝。
|
||||
ReasoningSignature string `json:"reasoning_signature,omitempty"`
|
||||
// RedactedThinking 是 Anthropic 的 redacted_thinking 数据(安全策略触发时),
|
||||
// 同样需要原样回传。
|
||||
RedactedThinking string `json:"redacted_thinking,omitempty"`
|
||||
|
||||
// --- 本地 trace:仅为度量与展示,绝不进入任何 API 请求报文 ---
|
||||
// 三家 provider 都手工白名单拼 body(oaBuildMessages / rsBuildInput /
|
||||
// anBuildMessages),全仓不存在 json.Marshal(Message),故这些字段不会出网;
|
||||
// 它们只随会话 JSONL 落盘。omitempty 保证旧消息与旧格式输出不变。
|
||||
StartedAt int64 `json:"started_at,omitempty"` // 本步开始时刻(unix ms)
|
||||
DurationMs int64 `json:"duration_ms,omitempty"` // 本步耗时:assistant = 模型调用,tool = 工具执行
|
||||
Step int `json:"step,omitempty"` // 第 N 次模型调用(tool 消息沿用其所属轮次)
|
||||
// Format/Model 是这一步的归因:协议格式 + 实际模型名。
|
||||
// 没有它们就分不清"哪一步换了供应商/模型"——真实会话里出现过一条中途
|
||||
// 切了 provider 的记录,事后只能靠 tool_call id 的前缀形态猜。
|
||||
Format string `json:"format,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
|
||||
// Tool 是 tool 消息对应的工具名。此前只有 ToolCallID 落盘,工具名得从失败块
|
||||
// 文本里 parse(retro.parseFriction 一直在这么干)。
|
||||
Tool string `json:"tool,omitempty"`
|
||||
// Fail 是归一出的失败类别(timeout / denied / self-kill / …),空 = 成功。
|
||||
// 有了它,stats 与 retro 不必再靠「[TOOL FAILED] …(kind)」字符串前缀计数——
|
||||
// 那条契约的原话是"内容前缀是唯一可靠信号",格式漂一处就静默失准。
|
||||
Fail string `json:"fail,omitempty"`
|
||||
// Exit 是命令类工具的退出码(取不到就不序列化)。
|
||||
Exit int `json:"exit,omitempty"`
|
||||
// Truncated 表示 Content 被长度上限截过:模型看到的不是全量,UI 该标出来,
|
||||
// 统计也知道这条会话里有多少结果其实被削了。
|
||||
Truncated bool `json:"truncated,omitempty"`
|
||||
// OutputLimited 是"**模型想写却写不下**":这一轮的终止原因是输出 token 上限
|
||||
// (OpenAI finish_reason=length / Anthropic stop_reason=max_tokens /
|
||||
// Responses response.incomplete + max_output_tokens)。
|
||||
//
|
||||
// 与 Truncated 正相反:那是我们自己削工具结果,这是模型的笔停了。不接住它的后果
|
||||
// 是"讲一半就停"——agent 的收尾判定是"没有 tool_calls ⇒ 说完了",于是半截答案
|
||||
// 被当成最终回复,用户只能自己打「继续」。带 tool_calls 的响应一律不算(见 completion)。
|
||||
OutputLimited bool `json:"output_limited,omitempty"`
|
||||
// Interrupted 是"这条没写完就断了":模型调用出错或被停止时,已经流出来的半截
|
||||
// 回复仍值得留下(见 agent.persistPartial),但它**不是**一个答案。
|
||||
// 没有这个标记,会话末尾看起来就像已经答完了,重试入口只能回一句
|
||||
// "没有可重试的消息"——而用户明明只看到一个断掉的半句。
|
||||
Interrupted bool `json:"interrupted,omitempty"`
|
||||
// Synthetic 标记这条 user 消息是系统生成的(后台作业完成通知等),不是人打的字。
|
||||
// 只用于本地展示与统计:provider 的手工白名单拼报文不会带上它;统计、rewind
|
||||
// 提问号与标题派生据此跳过,避免把通知算成一次人工干预。
|
||||
Synthetic bool `json:"synthetic,omitempty"`
|
||||
}
|
||||
|
||||
// ToolDef 向模型声明一个可调用工具。
|
||||
type ToolDef struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]any `json:"parameters"` // JSON schema
|
||||
}
|
||||
|
||||
// Usage 记录一次响应的 token 消耗。
|
||||
//
|
||||
// **口径约定:InputTokens 是本次请求在模型侧占位的提示 token 总量**,缓存命中
|
||||
// 与缓存写入都算在内,Cache* 字段只是明细。必须这么归一:Anthropic 的
|
||||
// `input_tokens` 只报**未命中缓存**的那一段(缓存部分单列在
|
||||
// cache_read_input_tokens / cache_creation_input_tokens)。照抄的后果是终端计数、
|
||||
// `stats` 的 TokensIn 与任何按 token 算的成本都被低报(95% 命中时 10 万的上下文
|
||||
// 记成 5 千)。注意:当前自动压缩走的是**字符估算**(见 agent/context.go),
|
||||
// 不依赖这几个数,所以这个口径修的是账目,不是压缩触发条件。
|
||||
// OpenAI / Responses 的 prompt_tokens 本身已含缓存,取其明细即可。
|
||||
type Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
|
||||
// CacheReadTokens 是命中提示缓存的 token 数(Anthropic cache_read /
|
||||
// OpenAI prompt_tokens_details.cached_tokens)。
|
||||
CacheReadTokens int `json:"cache_read_tokens,omitempty"`
|
||||
// CacheCreationTokens 是本次写入缓存的 token 数(Anthropic 独有,计费更贵)。
|
||||
CacheCreationTokens int `json:"cache_creation_tokens,omitempty"`
|
||||
// ReasoningTokens 是思考消耗(OpenAI/Responses 单列;Anthropic 混在
|
||||
// output_tokens 里报不出来,留 0,不要瞎猜)。
|
||||
ReasoningTokens int `json:"reasoning_tokens,omitempty"`
|
||||
}
|
||||
|
||||
// StreamDelta 是流式回调收到的一段增量内容。ToolCall 是一个工具调用的参数
|
||||
// 已经完整的提前通知:调用方可以在消息结束前先执行只读工具;最终
|
||||
// Message.ToolCalls 仍包含完整列表,按 ID 去重即可。
|
||||
type StreamDelta struct {
|
||||
Text string
|
||||
Reasoning string
|
||||
ToolCall *ToolCall
|
||||
Usage *Usage
|
||||
Err error
|
||||
}
|
||||
|
||||
// Provider 是统一的 LLM 后端接口。
|
||||
type Provider interface {
|
||||
// Stream 发起一轮对话,增量回调 delta 流;返回最终的 assistant 完整消息。
|
||||
// opts 为零值时不改变请求(思考级别默认关闭)。
|
||||
Stream(ctx context.Context, system string, msgs []Message, tools []ToolDef, opts StreamOptions, onDelta func(StreamDelta)) (Message, error)
|
||||
}
|
||||
|
||||
// ModelSwitcher 是支持运行时切换模型的 Provider。
|
||||
type ModelSwitcher interface {
|
||||
SetModel(model string)
|
||||
}
|
||||
|
||||
// CurrentModeler 是能报告当前模型的 Provider。模型预设路由(含图走多模态、
|
||||
// 失败回落备选)需要先知道现状才能决定切不切、以及回合后恢复到哪——
|
||||
// 只读查询,与 SetModel 配对使用。
|
||||
type CurrentModeler interface {
|
||||
CurrentModel() string
|
||||
}
|
||||
|
||||
// Identifier 是 provider 自报「协议格式 + 当前模型」的能力(三家都实现)。
|
||||
// Agent 用它给每条 assistant 消息盖归因戳:会话中途切了供应商或模型时,
|
||||
// 没有这两项就分不清哪一步是谁做的。
|
||||
type Identifier interface {
|
||||
Identity() (format, model string)
|
||||
}
|
||||
|
||||
// ModelLister 是支持枚举可用模型的 Provider(GET /models)。
|
||||
type ModelLister interface {
|
||||
ListModels(ctx context.Context) ([]string, error)
|
||||
}
|
||||
Reference in new issue
Block a user