package run import ( "context" "time" "gitea.stevedudenhoeffer.com/steve/majordomo/agent" ) // criticDeadlineCheck is how often the deadline-watch goroutine polls the // critic's hard deadline. Small relative to any realistic soft timeout. const criticDeadlineCheck = time.Second // criticBinding wires a CriticHandle into a run: the executor forwards activity // (steps + tool starts) to it, binds the run's hard cancellation to the critic's // extendable deadline, and exposes the critic's Steer messages as an agent // RunOption. All methods are nil-safe so the executor can call them // unconditionally when no critic is configured. type criticBinding struct { h CriticHandle } // startCritic begins critic monitoring for this run when one is configured and // the agent enables it. It launches a goroutine that cancels runCtx (via cancel) // the moment the critic's hard deadline passes — the critic may extend that // deadline, so a healthy-but-slow run is given room while a hung one is killed. // Returns (nil, no-op stop) when there is no critic. The caller MUST defer the // returned stop. func (e *Executor) startCritic(runCtx context.Context, cancel context.CancelFunc, ra RunnableAgent, info RunInfo) (*criticBinding, func()) { noop := func() {} if e.cfg.Ports.Critic == nil || !ra.Critic.Enabled { return nil, noop } soft := e.cfg.Defaults.CriticSoftTimeout if soft <= 0 { return nil, noop } h := e.cfg.Ports.Critic.Monitor(runCtx, info, soft) if h == nil { return nil, noop } done := make(chan struct{}) go func() { t := time.NewTicker(criticDeadlineCheck) defer t.Stop() for { select { case <-done: return case <-runCtx.Done(): return case <-t.C: // A zero deadline = no hard cap (not yet set); otherwise cancel // once we're at or past it. if d := h.Deadline(); !d.IsZero() && !time.Now().Before(d) { cancel() return } } } }() return &criticBinding{h: h}, func() { close(done) h.Stop() } } func (b *criticBinding) recordStep(iter int) { if b != nil { b.h.RecordStep(iter) } } func (b *criticBinding) recordToolStart(name, args string) { if b != nil { b.h.RecordToolStart(name, args) } } // steerOptions returns the agent RunOptions that drain the critic's steer // messages into the loop. Empty when there is no critic. func (b *criticBinding) steerOptions() []agent.RunOption { if b == nil { return nil } return []agent.RunOption{agent.WithSteer(b.h.Steer)} }