xai-org/grok-build · error

max turns reached

Error message

max turns reached

What it means

run_single_turn returns anyhow!("max turns reached") when the session ended with the MAX_TURNS_REACHED_CATEGORY stop reason: the agent consumed options.max_turns turns without finishing, so the run is treated as a failure after emitting on_max_turns and on_end.

Source

Thrown at crates/codegen/xai-grok-pager/src/headless.rs:1304

            {
                Some(r) => r,
                None => {
                    tracing::warn!(
                        "headless: prompt response carried no requestId; emitting an empty requestId"
                    );
                    ""
                }
            };
            let is_max_turns = resp
                .meta
                .as_ref()
                .and_then(|m| m.get(crate::app::CANCELLATION_CATEGORY_KEY))
                .and_then(|v| v.as_str())
                == Some(xai_grok_shell::session::commands::MAX_TURNS_REACHED_CATEGORY);
            if is_max_turns {
                emitter.on_max_turns();
                emitter.on_end(&stop_reason, sid, rid);
                Err(anyhow::anyhow!("max turns reached"))
            } else {
                emitter.on_end(&stop_reason, sid, rid);
                Ok(())
            }
        }
        Some(Err(err)) => {
            let msg = if i32::from(err.code) == RATE_LIMITED_ERROR_CODE {
                let detail = err.data.as_ref().and_then(error_detail_from_data);
                crate::app::sanitize_user_error(&format_rate_limited_user_message(
                    detail.as_deref(),
                    is_api_key_auth,
                ))
            } else {
                err.to_string()
            };
            if let Some(usage) = xai_grok_shell::sampling::error::prompt_usage_from_error(&err) {
                match serde_json::to_value(&usage) {
                    Ok(v) => emitter.usage = Some(v),

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Increase --max-turns to fit the task
  2. Simplify/narrow the prompt so fewer turns are needed
  3. Fix the underlying tool failure causing the turn loop
  4. Check session metadata for the category key to distinguish max-turns from other stops

Example fix

// before
pager --max-turns 3 -p "refactor the whole module"
// after
pager --max-turns 25 -p "refactor the whole module"
Defensive patterns

Strategy: fallback

Validate before calling

let estimated_turns = count_tool_calls_needed(prompt);
let max_turns = std::env::var("MAX_TURNS").ok()
    .and_then(|v| v.parse::<u32>().ok())
    .unwrap_or_else(|| estimated_turns.max(10));

Type guard

fn is_max_turns_reason(metadata: &serde_json::Value) -> bool {
    metadata.get("category").and_then(|c| c.as_str())
        == Some("max_turns_reached")
}

Try / catch

match run_single_turn(&mut session, &options).await {
    Err(e) if e.to_string() == "max turns reached" => {
        eprintln!("agent hit turn budget; increasing and retrying");
        options.max_turns *= 2;
        run_single_turn(&mut session, &options).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Setting --max-turns (options.max_turns) too low for the task; the agent loops on a failing tool call or repeatedly requests permission and burns turns; an interactive-ish task executed headlessly.

Common situations: CI runs with a small default max_turns on complex prompts; retry loops where the model keeps re-issuing the same failing command; permission prompts auto-denied headlessly consuming every turn.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/d5221d2649b70126. Report an issue: GitHub.