enforceMaxIterations runs at the top of every loop iteration. When the iteration count has reached the limit, it emits MaxIterationsReached and drives the appropriate exit/resume flow: - non-interactive sessions auto-stop with an assistant message, - interactive sessions block on r.resumeChan until
( ctx context.Context, sess *session.Session, a *agent.Agent, iteration int, runtimeMaxIterations int, events EventSink, )
| 46 | // tests that pump the resume channel and assert the resulting events |
| 47 | // without standing up a full model + toolset pipeline. |
| 48 | func (r *LocalRuntime) enforceMaxIterations( |
| 49 | ctx context.Context, |
| 50 | sess *session.Session, |
| 51 | a *agent.Agent, |
| 52 | iteration int, |
| 53 | runtimeMaxIterations int, |
| 54 | events EventSink, |
| 55 | ) (newMax int, decision iterationDecision) { |
| 56 | if runtimeMaxIterations <= 0 || iteration < runtimeMaxIterations { |
| 57 | return runtimeMaxIterations, iterationContinue |
| 58 | } |
| 59 | |
| 60 | slog.DebugContext(ctx, "Maximum iterations reached", |
| 61 | "agent", a.Name(), |
| 62 | "iterations", iteration, |
| 63 | "max", runtimeMaxIterations, |
| 64 | ) |
| 65 | |
| 66 | events.Emit(MaxIterationsReached(runtimeMaxIterations)) |
| 67 | |
| 68 | maxIterMsg := fmt.Sprintf("Maximum iterations reached (%d)", runtimeMaxIterations) |
| 69 | r.notifyMaxIterations(ctx, a, sess.ID, maxIterMsg) |
| 70 | r.executeOnUserInputHooks(ctx, sess.ID, "max iterations reached") |
| 71 | |
| 72 | stopMsg := fmt.Sprintf( |
| 73 | "Execution stopped after reaching the configured max_iterations limit (%d).", |
| 74 | runtimeMaxIterations, |
| 75 | ) |
| 76 | appendStopMsg := func() { |
| 77 | addAgentMessage(sess, a, &chat.Message{ |
| 78 | Role: chat.MessageRoleAssistant, |
| 79 | Content: stopMsg, |
| 80 | CreatedAt: r.now().Format(time.RFC3339), |
| 81 | }, events) |
| 82 | } |
| 83 | |
| 84 | // In non-interactive mode (e.g. MCP server), auto-stop instead of |
| 85 | // blocking forever waiting for user input. |
| 86 | if sess.NonInteractive { |
| 87 | slog.DebugContext(ctx, "Auto-stopping after max iterations (non-interactive)", "agent", a.Name()) |
| 88 | appendStopMsg() |
| 89 | return runtimeMaxIterations, iterationStop |
| 90 | } |
| 91 | |
| 92 | // Wait for user decision (resume / reject) |
| 93 | select { |
| 94 | case req := <-r.resumeChan: |
| 95 | if req.Type == ResumeTypeApprove { |
| 96 | slog.DebugContext(ctx, "User chose to continue after max iterations", "agent", a.Name()) |
| 97 | newMax := iteration + 10 |
| 98 | r.executeOnSessionResumeHooks(ctx, a, sess.ID, runtimeMaxIterations, newMax) |
| 99 | return newMax, iterationContinue |
| 100 | } |
| 101 | slog.DebugContext(ctx, "User rejected continuation", "agent", a.Name()) |
| 102 | appendStopMsg() |
| 103 | return runtimeMaxIterations, iterationStop |
| 104 | |
| 105 | case <-ctx.Done(): |