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
- Increase --max-turns to fit the task
- Simplify/narrow the prompt so fewer turns are needed
- Fix the underlying tool failure causing the turn loop
- 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
- Size max_turns to the task complexity, not the default
- Detect repeated identical tool calls to break loops early
- Monitor the max-turns metadata category in headless runs
- Keep prompts focused so fewer turns are consumed
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
- Headless mode requires a grok.com session. Run `grok login`
- command wrote more than {PROVIDER_STDOUT_CAP_BYTES} bytes to
- arboard unavailable
- headless: stdout write failed
- -32603
AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31).
Data as JSON: /api/errors/d5221d2649b70126.
Report an issue: GitHub.