Files
agentic-coding-workflow/internal/acp/supervisor.go
T

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).