autonomy policy: add configurable idle_round_budget auto-release window for no-dialog execution

This commit is contained in:
DBT
2026-03-01 16:08:26 +00:00
parent ed47e2dfe0
commit 5f9d526ef4
5 changed files with 23 additions and 2 deletions

View File

@@ -589,6 +589,9 @@ func summarizeAutonomyChanges(oldCfg, newCfg *config.Config) []string {
if o.WaitingResumeDebounceSec != n.WaitingResumeDebounceSec { if o.WaitingResumeDebounceSec != n.WaitingResumeDebounceSec {
changes = append(changes, "waiting_resume_debounce_sec") changes = append(changes, "waiting_resume_debounce_sec")
} }
if o.IdleRoundBudgetReleaseSec != n.IdleRoundBudgetReleaseSec {
changes = append(changes, "idle_round_budget_release_sec")
}
if strings.TrimSpace(o.QuietHours) != strings.TrimSpace(n.QuietHours) { if strings.TrimSpace(o.QuietHours) != strings.TrimSpace(n.QuietHours) {
changes = append(changes, "quiet_hours") changes = append(changes, "quiet_hours")
} }
@@ -967,6 +970,7 @@ func buildAutonomyEngine(cfg *config.Config, msgBus *bus.MessageBus) *autonomy.E
MaxRoundsWithoutUser: a.MaxRoundsWithoutUser, MaxRoundsWithoutUser: a.MaxRoundsWithoutUser,
TaskHistoryRetentionDays: a.TaskHistoryRetentionDays, TaskHistoryRetentionDays: a.TaskHistoryRetentionDays,
WaitingResumeDebounceSec: a.WaitingResumeDebounceSec, WaitingResumeDebounceSec: a.WaitingResumeDebounceSec,
IdleRoundBudgetReleaseSec: a.IdleRoundBudgetReleaseSec,
AllowedTaskKeywords: a.AllowedTaskKeywords, AllowedTaskKeywords: a.AllowedTaskKeywords,
ImportantKeywords: cfg.Agents.Defaults.Texts.AutonomyImportantKeywords, ImportantKeywords: cfg.Agents.Defaults.Texts.AutonomyImportantKeywords,
CompletionTemplate: cfg.Agents.Defaults.Texts.AutonomyCompletionTemplate, CompletionTemplate: cfg.Agents.Defaults.Texts.AutonomyCompletionTemplate,

View File

@@ -27,6 +27,7 @@
"max_rounds_without_user": 12, "max_rounds_without_user": 12,
"task_history_retention_days": 3, "task_history_retention_days": 3,
"waiting_resume_debounce_sec": 5, "waiting_resume_debounce_sec": 5,
"idle_round_budget_release_sec": 1800,
"allowed_task_keywords": [], "allowed_task_keywords": [],
"ekg_consecutive_error_threshold": 3 "ekg_consecutive_error_threshold": 3
}, },

View File

@@ -36,6 +36,7 @@ type Options struct {
QuietHours string QuietHours string
UserIdleResumeSec int UserIdleResumeSec int
WaitingResumeDebounceSec int WaitingResumeDebounceSec int
IdleRoundBudgetReleaseSec int
MaxRoundsWithoutUser int MaxRoundsWithoutUser int
TaskHistoryRetentionDays int TaskHistoryRetentionDays int
AllowedTaskKeywords []string AllowedTaskKeywords []string
@@ -111,6 +112,9 @@ func NewEngine(opts Options, msgBus *bus.MessageBus) *Engine {
if opts.MaxRoundsWithoutUser < 0 { if opts.MaxRoundsWithoutUser < 0 {
opts.MaxRoundsWithoutUser = 0 opts.MaxRoundsWithoutUser = 0
} }
if opts.IdleRoundBudgetReleaseSec < 0 {
opts.IdleRoundBudgetReleaseSec = 0
}
if opts.TaskHistoryRetentionDays <= 0 { if opts.TaskHistoryRetentionDays <= 0 {
opts.TaskHistoryRetentionDays = 3 opts.TaskHistoryRetentionDays = 3
} }
@@ -302,8 +306,15 @@ func (e *Engine) tick() {
continue continue
} }
if st.BlockReason == "idle_round_budget" && e.opts.MaxRoundsWithoutUser > 0 && e.roundsWithoutUser >= e.opts.MaxRoundsWithoutUser { if st.BlockReason == "idle_round_budget" && e.opts.MaxRoundsWithoutUser > 0 && e.roundsWithoutUser >= e.opts.MaxRoundsWithoutUser {
// Stay waiting until user activity resets round budget. // Optional auto-release without user dialog: allow one round after configured cooldown.
continue if e.opts.IdleRoundBudgetReleaseSec > 0 && !st.WaitingSince.IsZero() && now.Sub(st.WaitingSince) >= time.Duration(e.opts.IdleRoundBudgetReleaseSec)*time.Second {
e.roundsWithoutUser = e.opts.MaxRoundsWithoutUser - 1
e.writeReflectLog("resume", st, fmt.Sprintf("autonomy auto-resumed from idle round budget after %ds", e.opts.IdleRoundBudgetReleaseSec))
e.writeTriggerAudit("resume", st, "idle_round_budget_auto_release")
} else {
// Stay waiting until user activity resets round budget.
continue
}
} }
// Debounce waiting/resume flapping // Debounce waiting/resume flapping
if !st.WaitingSince.IsZero() && now.Sub(st.WaitingSince) < time.Duration(e.opts.WaitingResumeDebounceSec)*time.Second { if !st.WaitingSince.IsZero() && now.Sub(st.WaitingSince) < time.Duration(e.opts.WaitingResumeDebounceSec)*time.Second {

View File

@@ -58,6 +58,7 @@ type AutonomyConfig struct {
MaxRoundsWithoutUser int `json:"max_rounds_without_user" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_ROUNDS_WITHOUT_USER"` MaxRoundsWithoutUser int `json:"max_rounds_without_user" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_ROUNDS_WITHOUT_USER"`
TaskHistoryRetentionDays int `json:"task_history_retention_days" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_TASK_HISTORY_RETENTION_DAYS"` TaskHistoryRetentionDays int `json:"task_history_retention_days" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_TASK_HISTORY_RETENTION_DAYS"`
WaitingResumeDebounceSec int `json:"waiting_resume_debounce_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_WAITING_RESUME_DEBOUNCE_SEC"` WaitingResumeDebounceSec int `json:"waiting_resume_debounce_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_WAITING_RESUME_DEBOUNCE_SEC"`
IdleRoundBudgetReleaseSec int `json:"idle_round_budget_release_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_IDLE_ROUND_BUDGET_RELEASE_SEC"`
AllowedTaskKeywords []string `json:"allowed_task_keywords" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ALLOWED_TASK_KEYWORDS"` AllowedTaskKeywords []string `json:"allowed_task_keywords" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ALLOWED_TASK_KEYWORDS"`
EKGConsecutiveErrorThreshold int `json:"ekg_consecutive_error_threshold" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_EKG_CONSECUTIVE_ERROR_THRESHOLD"` EKGConsecutiveErrorThreshold int `json:"ekg_consecutive_error_threshold" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_EKG_CONSECUTIVE_ERROR_THRESHOLD"`
// Deprecated: kept for backward compatibility with existing config files. // Deprecated: kept for backward compatibility with existing config files.
@@ -378,6 +379,7 @@ func DefaultConfig() *Config {
MaxRoundsWithoutUser: 12, MaxRoundsWithoutUser: 12,
TaskHistoryRetentionDays: 3, TaskHistoryRetentionDays: 3,
WaitingResumeDebounceSec: 5, WaitingResumeDebounceSec: 5,
IdleRoundBudgetReleaseSec: 1800,
AllowedTaskKeywords: []string{}, AllowedTaskKeywords: []string{},
EKGConsecutiveErrorThreshold: 3, EKGConsecutiveErrorThreshold: 3,
}, },

View File

@@ -117,6 +117,9 @@ func Validate(cfg *Config) []error {
if aut.WaitingResumeDebounceSec <= 0 { if aut.WaitingResumeDebounceSec <= 0 {
errs = append(errs, fmt.Errorf("agents.defaults.autonomy.waiting_resume_debounce_sec must be > 0 when enabled=true")) errs = append(errs, fmt.Errorf("agents.defaults.autonomy.waiting_resume_debounce_sec must be > 0 when enabled=true"))
} }
if aut.IdleRoundBudgetReleaseSec < 0 {
errs = append(errs, fmt.Errorf("agents.defaults.autonomy.idle_round_budget_release_sec must be >= 0 when enabled=true"))
}
if aut.TaskHistoryRetentionDays <= 0 { if aut.TaskHistoryRetentionDays <= 0 {
errs = append(errs, fmt.Errorf("agents.defaults.autonomy.task_history_retention_days must be > 0 when enabled=true")) errs = append(errs, fmt.Errorf("agents.defaults.autonomy.task_history_retention_days must be > 0 when enabled=true"))
} }