You've already forked agentic-coding-workflow
138 lines
3.6 KiB
Go
138 lines
3.6 KiB
Go
// supervisor.go 管理 ACP agent 子进程池。每 session 一个 Process,spawn / kill /
|
|
// monitor 三类操作通过 mu 保护 procs map。
|
|
//
|
|
// 生命周期 invariants(spec §6.8):
|
|
// - procs 写锁仅在 spawn / Kill / monitorProcess delete 时持有
|
|
// - done channel 严格 close-once(monitor 唯一持有)
|
|
// - KilledByUs atomic 防 race(Kill / monitor 并发)
|
|
// - onExit 不能再 Lock supervisor.mu(避免死锁)
|
|
package acp
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"os/exec"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
|
|
"github.com/yan1h/agent-coding-workflow/internal/workspace"
|
|
)
|
|
|
|
// SupervisorConfig 是从 config.AcpConfig 派生的 supervisor 子集。
|
|
type SupervisorConfig struct {
|
|
SpawnTimeout time.Duration
|
|
KillGrace time.Duration
|
|
StderrBufferLines int
|
|
StderrTailBytes int
|
|
EventMaxPayload int
|
|
EventTruncateField int
|
|
ShutdownGrace time.Duration
|
|
}
|
|
|
|
// Supervisor 是 ACP 子进程总管。
|
|
type Supervisor struct {
|
|
mu sync.RWMutex
|
|
procs map[uuid.UUID]*Process
|
|
|
|
repo Repository
|
|
audit audit.Recorder
|
|
notify *notify.Dispatcher
|
|
wtSvc workspace.WorktreeService
|
|
wsRepo workspace.Repository
|
|
crypto *crypto.Encryptor
|
|
cfg SupervisorConfig
|
|
log *slog.Logger
|
|
}
|
|
|
|
// NewSupervisor 构造空 Supervisor。Start 不需要 — supervisor 是被动的(spawn 时
|
|
// 起 goroutine,无主循环)。Stop 在 ShutdownAll 中实现。
|
|
func NewSupervisor(repo Repository, rec audit.Recorder, disp *notify.Dispatcher,
|
|
wtSvc workspace.WorktreeService, wsRepo workspace.Repository,
|
|
enc *crypto.Encryptor, cfg SupervisorConfig, log *slog.Logger) *Supervisor {
|
|
if log == nil {
|
|
log = slog.Default()
|
|
}
|
|
return &Supervisor{
|
|
procs: map[uuid.UUID]*Process{},
|
|
repo: repo,
|
|
audit: rec,
|
|
notify: disp,
|
|
wtSvc: wtSvc,
|
|
wsRepo: wsRepo,
|
|
crypto: enc,
|
|
cfg: cfg,
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
// Process 是单个 agent 子进程的运行时句柄。
|
|
type Process struct {
|
|
SessionID uuid.UUID
|
|
WorkspaceID uuid.UUID
|
|
UserID uuid.UUID
|
|
IsMain bool // 主 worktree 占用 → release 走不同路径
|
|
WorktreeID *uuid.UUID // 子 worktree 时填,用于 release
|
|
|
|
Cmd *exec.Cmd
|
|
Stdin io.WriteCloser
|
|
Stdout io.ReadCloser
|
|
Stderr io.ReadCloser
|
|
Relay *Relay
|
|
|
|
StderrBuf *stderrRing // 排障 ring buffer
|
|
StartedAt time.Time
|
|
KilledByUs atomic.Bool // true → exited,false → crashed
|
|
done chan struct{} // monitor 退出时 close
|
|
}
|
|
|
|
// stderrRing 是固定行数 ring buffer,monitor 退出时取 tail 写 last_error。
|
|
type stderrRing struct {
|
|
mu sync.Mutex
|
|
lines []string
|
|
cap int
|
|
}
|
|
|
|
func newStderrRing(cap int) *stderrRing { return &stderrRing{cap: cap} }
|
|
|
|
func (r *stderrRing) Append(line string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if len(r.lines) >= r.cap {
|
|
r.lines = r.lines[1:]
|
|
}
|
|
r.lines = append(r.lines, line)
|
|
}
|
|
|
|
func (r *stderrRing) Tail(maxBytes int) string {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
out := ""
|
|
for i := len(r.lines) - 1; i >= 0 && len(out) < maxBytes; i-- {
|
|
out = r.lines[i] + "\n" + out
|
|
}
|
|
if len(out) > maxBytes {
|
|
out = out[len(out)-maxBytes:]
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GetRelay returns the relay for an active session, or nil if not running.
|
|
// WS handler 用此判断是否进 live 模式。
|
|
func (s *Supervisor) GetRelay(sid uuid.UUID) *Relay {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
p, ok := s.procs[sid]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return p.Relay
|
|
}
|
|
|
|
// Spawn / Kill / ShutdownAll / monitorProcess / handshake are implemented in subsequent tasks (E2).
|