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

+155
View File
@@ -0,0 +1,155 @@
package agent
import (
"errors"
"fmt"
"strings"
"time"
)
// failStreakWarn 是连续失败达到该次数后,在失败块里追加 [STUCK] 换思路提示。
const failStreakWarn = 3
// isTimeoutErr 识别"被时间限制杀掉"的错误形态。
// - signal: killed / context deadline exceeded:ctx 超时(bash 120s、fetch 20s…)
// - wait delay exceeded:进程已死,但它派生的进程仍占着输出管道,
// exec 的 WaitDelay 兜底生效(见 internal/execx)。
func isTimeoutErr(msg string) bool {
return strings.Contains(msg, "context deadline exceeded") ||
strings.Contains(msg, "signal: killed") ||
strings.Contains(msg, "wait delay exceeded")
}
// failInfo 是判断「这次失败到底是怎么回事」所需的上下文。光看错误文本不够:
// 同一个 "signal: killed" 可能是超时,也可能是用户按了停止。
type failInfo struct {
Tool string
Input string // 调用入参原文——识别 pkill 自匹配只能靠它
Elapsed time.Duration
Cancelled bool // 本回合被用户主动停止(不是超时)
}
// classifyFailure 按错误文本判定失败类别,并给出给模型看的针对性提示。
// 全部为英文:与系统提示词、既有 error 文案保持一致。
//
// Elapsed 只为一件事:**提示里不许出现编造的数字**。旧文案对所有工具硬写
// "bash limit 120s",而事故那一次 bash 实际挂了 23 分钟——模型据此判断
// "再试一次应该没事",人看到的也是错的诊断。
func classifyFailure(in failInfo, err error) (kind, hint string) {
if err == nil {
return "unknown", ""
}
msg := strings.ToLower(err.Error())
switch {
case in.Cancelled || strings.Contains(msg, "context canceled"):
// 用户按了停止:这不是"命令失败",不能让模型当成失败去重试同一件事。
// 必须排在 timeout 之前——被停止杀掉的进程报的也是 "signal: killed",
// 归成超时就等于告诉模型"这条命令太慢了",而它其实没问题。
return "cancelled", "The user stopped this turn, so the call was cancelled rather than failed. " +
"Do not retry it as if something was wrong with the command."
case strings.Contains(msg, "signal: terminated") && selfMatchedKiller(in.Input):
return "self-kill", selfKillHint
case isTimeoutErr(msg):
return "timeout", fmt.Sprintf("%s timed out after %s. Narrow the scope "+
"(limit paths, add --max-count, split the command) instead of rerunning it as-is; "+
"rerunning it unchanged will just time out again.", in.Tool, in.Elapsed.Round(time.Second))
case strings.Contains(msg, "dangerous action blocked"):
return "danger-blocked", "The irreversible-action gate stopped this. Say what you were about to do " +
"and why, then stop; do not attempt the same effect through another tool or a rewritten command."
case strings.Contains(msg, "exit status 127"):
return "not-found", "Command not found (exit 127). Check the spelling or use an absolute path."
case strings.Contains(msg, "exit status 126"):
return "not-executable", "Command found but not executable (exit 126). Check permissions or invoke it via its interpreter."
case strings.Contains(msg, "permission denied"):
return "permission", "Permission denied. Do not retry with sudo; check the path and whether the operation is allowed."
case strings.Contains(msg, "no such file or directory"):
return "missing-path", "Path does not exist. Confirm it with ls/read before retrying."
case strings.Contains(msg, "unknown tool"):
return "unknown-tool", "That tool does not exist. Check the available tool list before calling again."
case strings.Contains(msg, "user denied"):
return "denied", "The user denied this operation. Do not retry the same thing with a different wording."
case strings.Contains(msg, "not allowed by the active skill"):
return "skill-allowlist", "The active skill restricts which tools may be used. Stay within its allowed tools."
case strings.Contains(msg, "invalid") && (strings.Contains(msg, "arguments") || strings.Contains(msg, "input")):
return "bad-args", "Arguments did not match the tool schema. Re-read the parameter definition and fix the shape."
}
if code := exitCode(err); code > 0 {
return "nonzero-exit", fmt.Sprintf("Command exited with status %d. Read the output above to find the cause; do not rerun the same command unchanged.", code)
}
return "unknown", "The call failed. Read the message above, fix the root cause, then retry differently."
}
// exitCode 从 "exit status N" 形态的错误里取出退出码;取不到返回 -1。
func exitCode(err error) int {
var ee interface{ ExitCode() int }
if errors.As(err, &ee) {
return ee.ExitCode()
}
return -1
}
// formatToolFailure 拼装回灌给模型的失败块,并返回归一出的失败类别。
// out 是工具已产出的部分输出(可能为 ""),会原样保留在失败块之前,避免丢失
// 有价值的 stdout。kind 除了进文本,还落进会话消息的结构字段(见 llm.Message.Fail),
// 让 stats/retro 不必再解析字符串前缀。
func formatToolFailure(in failInfo, err error, out string, streak int) (string, string) {
var b strings.Builder
if strings.TrimSpace(out) != "" {
b.WriteString(strings.TrimRight(out, "\n"))
b.WriteString("\n")
}
kind, hint := classifyFailure(in, err)
fmt.Fprintf(&b, "[TOOL FAILED] %s: %s (%s)\n", in.Tool, err.Error(), kind)
if hint != "" {
fmt.Fprintf(&b, "[hint] %s\n", hint)
}
if streak >= failStreakWarn {
fmt.Fprintf(&b, "[STUCK] %s has now failed %d times in a row. Stop repeating the same approach: "+
"switch tool or change the arguments, re-verify your assumptions, or update the plan.\n", in.Tool, streak)
}
return strings.TrimRight(b.String(), "\n"), kind
}
// selfKillHint 针对的是 agent 跑 shell 时一个几乎必中的坑(实测两次,都只报
// unknown,模型看不出门道)。
const selfKillHint = "The command killed the shell that was running it: pkill/killall/pgrep with -f match " +
"full command lines, and this command's own line contains the pattern you searched for — so it terminated " +
"itself and nothing after it ran. Avoid self-matching: bracket the first letter (`[b]ili-coin-dl` matches " +
"the process but not your own command line), match an exact name with -x, kill a recorded pid, or start the " +
"job with bash_bg and stop it by id."
// selfMatchedKiller 认「用 -f 模式杀进程,而模式串就写在自己的命令行里」。
//
// `sh -c "pkill -f myapp; …"` 的完整命令行包含 "pkill -f myapp",其中就有 "myapp",
// 于是 pkill 先把执行它的 shell 杀了(实测 15ms 就 signal: terminated),后面的
// cat/校验/收尾一个字节都没执行。只有 -f(全命令行匹配)有这个问题,-x 按名字匹配不会。
func selfMatchedKiller(input string) bool {
l := strings.ToLower(input)
if !strings.Contains(l, "-f") {
return false
}
return strings.Contains(l, "pkill") || strings.Contains(l, "killall") || strings.Contains(l, "pgrep")
}
// noteToolFailure 记录一次工具失败,返回该工具当前的连续失败次数。
// 按**工具名**计数:agent 重试时通常会微调参数,按「工具+参数」会漏判,
// 而全局计数又太噪。
func (a *Agent) noteToolFailure(tool string) int {
a.failMu.Lock()
defer a.failMu.Unlock()
if a.failStreak == nil {
a.failStreak = map[string]int{}
}
a.failStreak[tool]++
return a.failStreak[tool]
}
// noteToolSuccess 在工具成功时清零该工具的连续失败计数(说明它有进展)。
func (a *Agent) noteToolSuccess(tool string) {
a.failMu.Lock()
defer a.failMu.Unlock()
if a.failStreak == nil {
return
}
delete(a.failStreak, tool)
}