You've already forked agentic-coding-workflow
feat(acp): supervisor skeleton + AcpConfig wiring
This commit is contained in:
@@ -107,3 +107,20 @@ notify:
|
|||||||
secret: ""
|
secret: ""
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
topics: []
|
topics: []
|
||||||
|
|
||||||
|
# ACP module: agent subprocess pool + WebSocket fanout + event retention
|
||||||
|
acp:
|
||||||
|
enabled: true
|
||||||
|
max_active_global: 20
|
||||||
|
max_active_per_user: 3
|
||||||
|
spawn_timeout: 30s
|
||||||
|
kill_grace: 5s
|
||||||
|
stderr_buffer_lines: 100
|
||||||
|
stderr_tail_bytes: 2000
|
||||||
|
ws_ping_interval: 30s
|
||||||
|
ws_pong_timeout: 60s
|
||||||
|
ws_send_buffer: 256
|
||||||
|
event_max_payload_bytes: 65536
|
||||||
|
event_truncate_field_bytes: 4096
|
||||||
|
event_retention_days: 7
|
||||||
|
shutdown_grace: 30s
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relay field + GetRelay method added in Task D7 when Relay type is wired.
|
||||||
|
|
||||||
|
// Spawn / Kill / ShutdownAll / monitorProcess / handshake are implemented in subsequent tasks (E2).
|
||||||
@@ -41,6 +41,7 @@ type Config struct {
|
|||||||
Storage Storage `mapstructure:"storage"`
|
Storage Storage `mapstructure:"storage"`
|
||||||
Jobs JobsConfig `mapstructure:"jobs"`
|
Jobs JobsConfig `mapstructure:"jobs"`
|
||||||
Notify NotifyConfig `mapstructure:"notify"`
|
Notify NotifyConfig `mapstructure:"notify"`
|
||||||
|
Acp AcpConfig `mapstructure:"acp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPConfig 描述 HTTP 服务监听参数。
|
// HTTPConfig 描述 HTTP 服务监听参数。
|
||||||
@@ -165,6 +166,24 @@ type WebhookCfg struct {
|
|||||||
Topics []string `mapstructure:"topics"`
|
Topics []string `mapstructure:"topics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AcpConfig 控制 ACP 模块的并发限流、子进程超时、WS 心跳与事件保留。
|
||||||
|
type AcpConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
MaxActiveGlobal int `mapstructure:"max_active_global"`
|
||||||
|
MaxActivePerUser int `mapstructure:"max_active_per_user"`
|
||||||
|
SpawnTimeout time.Duration `mapstructure:"spawn_timeout"`
|
||||||
|
KillGrace time.Duration `mapstructure:"kill_grace"`
|
||||||
|
StderrBufferLines int `mapstructure:"stderr_buffer_lines"`
|
||||||
|
StderrTailBytes int `mapstructure:"stderr_tail_bytes"`
|
||||||
|
WSPingInterval time.Duration `mapstructure:"ws_ping_interval"`
|
||||||
|
WSPongTimeout time.Duration `mapstructure:"ws_pong_timeout"`
|
||||||
|
WSSendBuffer int `mapstructure:"ws_send_buffer"`
|
||||||
|
EventMaxPayload int `mapstructure:"event_max_payload_bytes"`
|
||||||
|
EventTruncateField int `mapstructure:"event_truncate_field_bytes"`
|
||||||
|
EventRetentionDays int `mapstructure:"event_retention_days"`
|
||||||
|
ShutdownGrace time.Duration `mapstructure:"shutdown_grace"`
|
||||||
|
}
|
||||||
|
|
||||||
// Load 从(按优先级递增)默认值 → 配置文件(如有)→ 环境变量 加载配置。
|
// Load 从(按优先级递增)默认值 → 配置文件(如有)→ 环境变量 加载配置。
|
||||||
// configFile 可为空字符串,仅用环境变量。
|
// configFile 可为空字符串,仅用环境变量。
|
||||||
func Load(configFile string) (*Config, error) {
|
func Load(configFile string) (*Config, error) {
|
||||||
@@ -210,6 +229,20 @@ func Load(configFile string) (*Config, error) {
|
|||||||
v.SetDefault("jobs.job_purge.completed_retention", "168h")
|
v.SetDefault("jobs.job_purge.completed_retention", "168h")
|
||||||
v.SetDefault("notify.webhook.enabled", false)
|
v.SetDefault("notify.webhook.enabled", false)
|
||||||
v.SetDefault("notify.webhook.timeout", "10s")
|
v.SetDefault("notify.webhook.timeout", "10s")
|
||||||
|
v.SetDefault("acp.enabled", true)
|
||||||
|
v.SetDefault("acp.max_active_global", 20)
|
||||||
|
v.SetDefault("acp.max_active_per_user", 3)
|
||||||
|
v.SetDefault("acp.spawn_timeout", "30s")
|
||||||
|
v.SetDefault("acp.kill_grace", "5s")
|
||||||
|
v.SetDefault("acp.stderr_buffer_lines", 100)
|
||||||
|
v.SetDefault("acp.stderr_tail_bytes", 2000)
|
||||||
|
v.SetDefault("acp.ws_ping_interval", "30s")
|
||||||
|
v.SetDefault("acp.ws_pong_timeout", "60s")
|
||||||
|
v.SetDefault("acp.ws_send_buffer", 256)
|
||||||
|
v.SetDefault("acp.event_max_payload_bytes", 65536)
|
||||||
|
v.SetDefault("acp.event_truncate_field_bytes", 4096)
|
||||||
|
v.SetDefault("acp.event_retention_days", 7)
|
||||||
|
v.SetDefault("acp.shutdown_grace", "30s")
|
||||||
|
|
||||||
if configFile != "" {
|
if configFile != "" {
|
||||||
v.SetConfigFile(configFile)
|
v.SetConfigFile(configFile)
|
||||||
|
|||||||
Reference in New Issue
Block a user