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

+83
View File
@@ -0,0 +1,83 @@
// Package execx 执行子进程,并保证「超时/取消」真的生效。
//
// 标准库的 exec.CommandContext 在取消时只 Kill **直接子进程**。而
// `sh -c "git clone …"` 里 git 还要派生 git-remote-https,孙子继承了 stdout
// 管道的写端;只要写端还被人拿着,Go 的输出拷贝 goroutine 就不会结束,
// cmd.Run() 也一直不返回——于是"120 秒超时"实测能挂住 23 分钟
// (真实事故:session 20261008-214717,一条 git clone 之后整回合静止)。
//
// 本包用两件事收口,缺一不可:
// - 子进程自成进程组,取消时 kill(-pgid) 带走整棵树(治孤儿进程);
// - WaitDelay 兜底:即使还有别的挂住的 fd 占着管道,Wait 也必定在
// 超时后有限时间内返回(治调用方被钉死)。
package execx
import (
"bytes"
"context"
"os/exec"
"time"
)
// WaitGrace 是命令已退出/已被杀之后,再等在途输出拷贝收尾的宽限期。
// 超过它就放弃等待直接返回,绝不让调用方被孙进程占住的管道钉死。
const WaitGrace = 2 * time.Second
// Options 是一次执行的可选参数;零值即用默认(继承环境、继承 cwd、无输入)。
type Options struct {
Dir string // 工作目录
Env []string // 环境表;nil = 继承当前进程环境
// Stdin 标准输入。用 []byte 而不是 io.Reader:调用方大多已经手握着内容,
// 而 io.Reader 会让 Run 无法重试语义之外的复用(当前没有复用需求)。
Stdin []byte
}
// Command 构造一条命令:ctx 取消即杀掉整棵进程树,Wait 有有限上界。
// 需要自定义 Stdout/Stderr 为别的 writer 时用它;一次性取输出用 Run。
func Command(ctx context.Context, name string, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, name, args...)
SeparateGroup(cmd)
// 默认 Cancel 只 Process.Kill(),正是孤儿问题的来源。
cmd.Cancel = func() error { return KillTree(cmd) }
cmd.WaitDelay = WaitGrace
return cmd
}
// Run 执行命令并返回合并的 stdout+stderr。超时由 ctx 决定。
// 输出需要截断时配合 Truncate(它不会把一个 UTF-8 字符切两半)。
func Run(ctx context.Context, opts Options, name string, args ...string) ([]byte, error) {
cmd := Command(ctx, name, args...)
if opts.Dir != "" {
cmd.Dir = opts.Dir
}
if opts.Env != nil {
cmd.Env = opts.Env
}
if opts.Stdin != nil {
cmd.Stdin = bytes.NewReader(opts.Stdin)
}
var buf bytes.Buffer
cmd.Stdout, cmd.Stderr = &buf, &buf
err := cmd.Run()
return buf.Bytes(), err
}
// Shell 执行一段 shell 脚本(等价 sh -c script)。
func Shell(ctx context.Context, opts Options, script string) ([]byte, error) {
return Run(ctx, opts, "sh", "-c", script)
}
// Truncate 把输出截到 maxBytes 以内,并报告是否发生了截断。
//
// 必须退到字符边界:中文一个字占 3 字节,按字节硬切会把最后一个字劈开,
// 尾部留下非法 UTF-8——它进 JSON 会变成替换字符,也可能被模型网关直接拒掉。
func Truncate(b []byte, maxBytes int) ([]byte, bool) {
if maxBytes <= 0 || len(b) <= maxBytes {
return b, false
}
cut := maxBytes
for cut > 0 && b[cut]&0xC0 == 0x80 {
cut-- // b[cut] 是 UTF-8 续字节:退到本字符的首字节
}
return b[:cut], true
}