Files

92 lines
2.2 KiB
Go

package flow
import (
"context"
"sync"
"time"
)
// idleCancel wraps a resettable idle timer. If Reset() is not called before
// the timer fires, the underlying context is cancelled.
type idleCancel struct {
cancel context.CancelFunc
timer *time.Timer
mu sync.Mutex
fired bool
baseTimeout time.Duration
toolCalls int
}
func (ic *idleCancel) Reset() {
ic.mu.Lock()
defer ic.mu.Unlock()
if !ic.fired {
ic.timer.Stop()
ic.timer.Reset(ic.currentTimeout())
}
}
// RecordToolCall increments the tool call counter and extends the idle timeout.
func (ic *idleCancel) RecordToolCall() {
ic.mu.Lock()
defer ic.mu.Unlock()
ic.toolCalls++
}
func (ic *idleCancel) Stop() {
ic.mu.Lock()
defer ic.mu.Unlock()
ic.timer.Stop()
}
func (ic *idleCancel) DidFire() bool {
ic.mu.Lock()
defer ic.mu.Unlock()
return ic.fired
}
// ToolCalls returns the number of tool calls recorded.
func (ic *idleCancel) ToolCalls() int {
ic.mu.Lock()
defer ic.mu.Unlock()
return ic.toolCalls
}
// currentTimeout returns the adaptive timeout: base + 60s per tool call, capped at 600s.
// Tool calls (especially spawn/subagent) can take minutes to complete, so the
// extension per tool call is generous to avoid interrupting active work.
func (ic *idleCancel) currentTimeout() time.Duration {
extra := time.Duration(ic.toolCalls) * 60 * time.Second
timeout := ic.baseTimeout + extra
if timeout > 600*time.Second {
timeout = 600 * time.Second
}
return timeout
}
const defaultIdleTimeout = 90 * time.Second
// withIdleTimeout returns a context that is cancelled if no Reset() call is
// made within the adaptive idle timeout. The returned idleCancel must have
// Reset() called for each meaningful event to prevent the timeout from firing.
// The timeout adapts: base + 10s per tool call, capped at 300s.
func withIdleTimeout(parent context.Context, baseTimeout ...time.Duration) (context.Context, *idleCancel) {
bt := defaultIdleTimeout
if len(baseTimeout) > 0 && baseTimeout[0] > 0 {
bt = baseTimeout[0]
}
ctx, cancel := context.WithCancel(parent)
ic := &idleCancel{
cancel: cancel,
baseTimeout: bt,
}
ic.timer = time.AfterFunc(bt, func() {
ic.mu.Lock()
ic.fired = true
ic.mu.Unlock()
cancel()
})
return ctx, ic
}