diff --git a/agent/loop.go b/agent/loop.go index f6366d5..f102280 100644 --- a/agent/loop.go +++ b/agent/loop.go @@ -667,17 +667,24 @@ func run(ctx context.Context, cfg agentConfig, state state) error { } } - // Soft step-budget guardrail: when MaxSteps is set and the budget is - // exhausted, nudge the model to wrap up and exit cleanly. This is not - // a hard abort — the model gets one final turn without tools to emit - // a summary or hand-off message. - if cfg.MaxSteps > 0 && step >= cfg.MaxSteps-1 { - emit(cfg, Event{Role: "maxsteps", Content: fmt.Sprintf("%d", step+1)}) - state.update(backend.Message{ - Role: "user", - Content: fmt.Sprintf("[system: step budget exhausted (%d/%d steps used). Stop calling toolsrv. Summarize what you have done and what remains, then stop.]", step+1, cfg.MaxSteps), - }, nil) - break + // Step budget warnings: nudge the agent at 50% and when exhausted. + // These only fire for research-heavy loops — action tools (file_write, + // file_edit, shell) reset the counter, so active progress never triggers. + if cfg.MaxSteps > 0 { + halfBudget := cfg.MaxSteps / 2 + if step == halfBudget { + state.update(backend.Message{ + Role: "user", + Content: fmt.Sprintf("[system: you have used %d/%d research steps without taking action. Consider making progress — write code, edit files, or run commands. Action tools reset this counter.]", step, cfg.MaxSteps), + }, nil) + } else if step >= cfg.MaxSteps-1 { + emit(cfg, Event{Role: "maxsteps", Content: fmt.Sprintf("%d", step+1)}) + state.update(backend.Message{ + Role: "user", + Content: fmt.Sprintf("[system: step budget exhausted (%d/%d steps used). Stop calling tools. Summarize what you have done and what remains, then stop.]", step+1, cfg.MaxSteps), + }, nil) + break + } } // Periodic task state re-injection: every N tool rounds, surface the