Why: Q1 WP5 - platform's SQLite becomes an implementation of the squad Store port so run state and orchestration state live in one database, and platform's in-process execution goes through the Executor port instead of bypassing it (X6). Evidence: all five modules go vet clean and go test ok, including engine-on-SQLite (state in DB, no run-level JSON files, second engine recovers from the same DB), SQLiteStore round trip, and the full squad compatibility net. Impact: runState/hitlGate exported so the port is implementable; Options.Store wires into engineDeps; leader persistence and checkpoints route through the port; PrepareWorkspace no longer writes run-level JSON (st.save owns it); platform RunInProcess takes squad.Executor (callers updated); serve mounts the engine on platform's RunStore - single orchestration.db for both. Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md Q1 WP5
121 lines
3.1 KiB
Go
121 lines
3.1 KiB
Go
package squad
|
||
|
||
import (
|
||
"fmt"
|
||
"time"
|
||
)
|
||
|
||
// Termination 运行级停止条件的单一注册点(Q1 WP6):leader 轮次、修复预算、
|
||
// 唤醒次数与步骤超时集中声明;复合判定用 And/Or 原语(AutoGen 终止条件语义)。
|
||
// 默认值与现网一致;引擎选项在 terminationOf 覆盖对应字段。
|
||
type Termination struct {
|
||
MaxLeaderTurns int
|
||
RepairBudget int
|
||
MaxRepairGranted int // 人工 retry 累计加预算上限(Q26):默认 RepairBudget*2
|
||
MaxWakeupFires int
|
||
StepTimeout time.Duration
|
||
}
|
||
|
||
// DefaultTermination 与现网四处的硬编码取值一致。
|
||
func DefaultTermination() Termination {
|
||
return Termination{
|
||
MaxLeaderTurns: DefaultMaxLeaderTurns,
|
||
RepairBudget: 1,
|
||
MaxRepairGranted: 2,
|
||
MaxWakeupFires: MaxWakeupFires,
|
||
StepTimeout: 10 * time.Minute,
|
||
}
|
||
}
|
||
|
||
// terminationOf 取本运行的停止条件:引擎选项与运行状态覆盖默认值。
|
||
func (e *SquadEngine) terminationOf(st *RunState) Termination {
|
||
t := DefaultTermination()
|
||
if e.opts.RepairBudget > 0 {
|
||
t.RepairBudget = e.opts.RepairBudget
|
||
t.MaxRepairGranted = e.opts.RepairBudget * 2
|
||
}
|
||
if e.opts.StepTimeout > 0 {
|
||
t.StepTimeout = e.opts.StepTimeout
|
||
}
|
||
if st != nil && st.Leader != nil && st.Leader.MaxTurns > 0 {
|
||
t.MaxLeaderTurns = st.Leader.MaxTurns
|
||
}
|
||
return t
|
||
}
|
||
|
||
// Counters 运行期计数,由各判定处喂入(值语义快照)。
|
||
type Counters struct {
|
||
LeaderTurns int
|
||
RepairSpent int
|
||
RepairGranted int
|
||
WakeupFires int
|
||
Deadline time.Time
|
||
}
|
||
|
||
// Cond 是一条停止条件:返回(原因,是否命中)。
|
||
type Cond func(c Counters, now time.Time) (string, bool)
|
||
|
||
// MaxTurnsC 轮次上限。
|
||
func MaxTurnsC(n int) Cond {
|
||
return func(c Counters, _ time.Time) (string, bool) {
|
||
if c.LeaderTurns >= n {
|
||
return fmt.Sprintf("leader 轮次达上限 %d", n), true
|
||
}
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
// RepairBudgetC 修复预算耗尽。
|
||
func RepairBudgetC(n int) Cond {
|
||
return func(c Counters, _ time.Time) (string, bool) {
|
||
if c.RepairSpent > n {
|
||
return fmt.Sprintf("修复预算耗尽(%d/%d)", c.RepairSpent, n), true
|
||
}
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
// WakeupsC 自动唤醒次数超限。
|
||
func WakeupsC(n int) Cond {
|
||
return func(c Counters, _ time.Time) (string, bool) {
|
||
if c.WakeupFires > n {
|
||
return fmt.Sprintf("自动唤醒次数超限(%d/%d)", c.WakeupFires, n), true
|
||
}
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
// DeadlineC 超过截止时刻。
|
||
func DeadlineC() Cond {
|
||
return func(c Counters, now time.Time) (string, bool) {
|
||
if !c.Deadline.IsZero() && now.After(c.Deadline) {
|
||
return "超过截止时刻", true
|
||
}
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
// And 全部条件命中才成立。
|
||
func And(conds ...Cond) Cond {
|
||
return func(c Counters, now time.Time) (string, bool) {
|
||
for _, k := range conds {
|
||
if _, hit := k(c, now); !hit {
|
||
return "", false
|
||
}
|
||
}
|
||
return "all conditions met", true
|
||
}
|
||
}
|
||
|
||
// Or 任一条件命中即成立(默认的停止语义)。
|
||
func Or(conds ...Cond) Cond {
|
||
return func(c Counters, now time.Time) (string, bool) {
|
||
for _, k := range conds {
|
||
if r, hit := k(c, now); hit {
|
||
return r, true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
}
|