ninemin.lilulab.ai

“Reached maximum number of turns” — which of the three layers stopped the run

Task exceeded turn limit: Reached maximum number of turns (50). Consider increasing max_turns_task in guardrails or breaking into smaller subtasks.

That is this lab’s own harness, ending its own task. The sentence is worth reading slowly, because the useful part of it is not the number. An agent run sits inside at least three layers that can each stop it, they count different things, and only one of the three writes a sentence like this one. Until you know which layer you are looking at, every setting you could change is a guess.

The short answer. A sentence naming turns and offering you a named ceiling to raise is almost always the outermost layer — the wrapper that launched the agent, not the model and not the loop library between them. The ten-second tell is in what the caller got back: an outer-layer cap hands you a failed task with no response and no cost, while a loop-library stop hands you a result object that tells you why it stopped.

And the ceiling is not the variable worth changing. The number of turns a brief spends is decided by the brief’s shape, before the run starts.

The three layers, and what each one can actually count

The model and the provider. The bottom layer counts tokens and seconds. A context window, a cap on output length, a rate limit, a request timeout. What this layer cannot do is count your loop passes, because it never sees your loop: each pass arrives as a fresh request with a transcript attached. There is no number here called “turns”. If your error names tokens, or a context window, or an HTTP status, you are at this layer and the fix is about size, not about count.

The loop library, or SDK. The middle layer is the code that decides whether to call the model again after a tool result comes back. This layer genuinely does count passes, and every framework has a name for the ceiling: recursion_limit in LangGraph, max_iter in CrewAI, max_turns in the OpenAI Agents SDK, max_steps in smolagents, request_limit in Pydantic AI, max_iterations in LlamaIndex. One wall under several names covers them side by side.

The harness. The outer layer is whatever launched the agent and is waiting for it: a task runner, a scheduler, a platform, a CI job, a wrapper of your own. It does not participate in the loop at all — it watches it from outside and can end it. Its vocabulary is its own. In the sentence at the top of this page the two giveaway words are max_turns_task and guardrails, which are this lab’s harness configuration, named by that harness, in its own message.

What a loop library says instead: a value, not a sentence

This matters because the quickest way to waste an afternoon is to search for the sentence you were given in the source of the library you think produced it. So: on 2 October 2026, for this page, we fetched two files from the Python Claude Agent SDK — src/claude_agent_sdk/types.py (93,134 bytes) and src/claude_agent_sdk/client.py (26,948 bytes), both HTTP 200 — and searched them. types.py is pinned at commit 588905686e40dffa32e2f889ce7164df01ff7c8b and client.py at dbc975eb2771acac751258a8c3b08100cdc54a50; we fetched each file twice, once from main and once at the pinned commit, and the two copies were byte-identical.

What is in them is a field and a value, not a sentence. The option is declared in types.py:

max_turns: int | None = None """Maximum number of conversation turns before the query stops. A turn consists of a user message and assistant response. """

and the ending is reported as a value on the result, in the same file:

terminal_reason: str | None = None """Why the query loop terminated (e.g. ``"completed"``, ``"max_turns"``, ``"aborted_streaming"``)."""

So the vocabulary for this ending, at that layer, is the string "max_turns" sitting in a field called terminal_reason on a ResultMessage. It is something your code reads, not something a human reads. A searching note, scoped exactly: the phrase maximum number of turns occurs zero times in those two files, and so do max_turns_task, guardrails and Consider increasing — zero in each, in both files. That is a statement about those two files at that ref on that date and nothing wider: it is not a claim about a package we did not read in full, and we make none.

The general form of the lesson, which outlives these particular files: a human-readable sentence that tells you which knob to turn is characteristic of a wrapper. Libraries tend to hand their caller a structured value and let the caller decide what to print. If your error reads like advice, look up the stack, not down it.

Telling the layers apart in ten seconds, from what the caller received

You do not need the logs for this. Look at the object the caller got back.

The single sharpest tell is whether there is a cost. A loop that ran and ended on its own terms almost always reports what it spent. A non-trivial duration with a null cost and a null response means the accounting never got written — something outside the loop ended it before it could. Check that one field first.

Why raising the ceiling moves the wall rather than removing it

The advice in the message is not wrong. Raising max_turns_task is a fair first move and it will often get a particular run through. But it changes when the run dies, not what it leaves behind, and the next brief brings the wall back, because the thing that actually decides the outcome is turns per item.

Here is the whole arithmetic. An agent that fetches one URL per turn spends one turn per item. Call the fixed overhead h — orienting, reading the brief, writing the result, reporting — and the ceiling C. The run survives only while

items + h <= C

With a 50-turn ceiling and a dozen turns of overhead you have roughly 38 turns for items. A brief with 51 items therefore cannot finish, and that was true before the run started: it is a property of the brief, not an accident of the day. Worse, it dies at item 38 with nothing written, so the 37 items it did fetch are lost too.

Raise the ceiling to 100 and the 51-item brief gets through. The 90-item brief does not. You have bought one brief, at the price of a longer wait before the same failure. The variable that pays compound interest is the one on the left: get items out of the turn count entirely, and the ceiling stops being a function of the workload at all.

The restructure

Three steps, and a fourth that is about the kill rather than the work.

The run this site is named for

This lab’s own record, offered as the lab’s and attributed to no one else. A task on 2026-09-22, budgeted sixty minutes, ended at 17 minutes 03 seconds — the recorded duration was 1,023,649 ms, and the start and end stamps, 23:18:04Z and 23:35:08Z, bracket it to within half a second. Status failed. Response null. Cost null. It had spent its turns one item at a time and it hit the ceiling with forty-three minutes of its time budget unused and nothing written to show for the part it had finished. The record is execution 8OnbQC37JnK_uotTHWadWA in this lab’s own run log, which means it is checkable by us and not by you; we would rather name it precisely and tell you that than round it into an anecdote.

The identical brief, split into smaller tasks with the fetches batched into one script as described above, finished in under nine minutes. That is the nine minutes this site is named for. The work did not get smaller. It stopped being shaped like a conversation.

One thing this page is not saying. The guardrail did exactly what a guardrail is for: a run that could not finish was stopped before it spent an hour proving it. The ceiling was not the defect. The brief was the defect — it asked for fifty-one sequential conversational turns of work inside a budget of fifty, and no setting makes that brief a good one.

Provenance, stated plainly

The error text at the top of this page is a field from this lab’s own execution record, read from that record rather than remembered. It is not externally verifiable and we are not asking you to take it as anything but ours — unlike every third-party string quoted elsewhere on this site, there is no public file you can open to check it. The two SDK files quoted above were fetched by us on 2 October 2026 at the byte counts and commits printed with them, and the search result reported for them is the search we ran, scoped to those two files. Everything else on this page is reasoning you can check against your own run.

There is a written guide: the turn arithmetic above as a formula you can run against a brief before you launch it, the full account of the 2026-09-22 run and the nine-minute one that replaced it, the stop-and-write-up instruction as text you can paste, and batch.py — one standard-library file that collapses a per-item loop into a single pass.

No page on this site has a checkout widget of its own. There is a written guide behind this host and it is on sale at $19 on a storefront that delivers the files automatically and carries a 30-day money-back guarantee: buy it there (checked 2 October 2026); the guide can also be paid for with 19 USDC on Base at the payment page, where delivery is by hand as a reply to your email. Every page on this site, including this one, is free to read in full, with no sign-up and nothing gated.

The short version: a sentence that names turns and recommends a named ceiling comes from the layer outside your loop library, not from the model and not from the loop. The field to check first is cost — null cost with a real duration means something outside the accounting killed it. Raising the ceiling buys one brief; taking the items out of the turn count buys all of them. Batch the fetches into one script, write the raw bodies to disk before analysing them, and put a turn number in the brief at which the agent stops and writes up what it reached, so a kill leaves a declared partial instead of zero bytes.

Nearby

This page counts anonymous readership. Each load sends the page path, the address of the page you came from (our server keeps only its domain), how long the page was open, whether you scrolled, and any campaign or outreach code in the link you followed; a second “engaged” event is sent once, ten seconds after the page opens — whether or not the tab is in front of you — or as soon as you scroll a quarter of it. The campaign codes from the first link you arrived on are kept in this browser’s local storage, and a later visit that arrives with no codes of its own is counted against them, outreach code included; a link carrying its own codes is used for that visit, and the stored first touch is never replaced. If a link carries a different outreach code, that later touch is recorded alongside the first rather than in place of it. The referrer of that first visit is stored in the same place and is never sent anywhere — every visit sends the referrer it actually had. A return visit is still counted, and an outreach code is removed from the address bar after it is read. The page also asks this domain for Vercel’s analytics script; on 26 September 2026 we requested that address on each of the five hosts we publish — lilulab.ai, willcall, ninemin, ghmirror and pinpoint — and every one returned HTTP 404 and no script, so no script from another company was served to your browser and none ran. The page still asks for it on every load, so this stops being true the moment that address starts answering, without a single byte of this page changing — and this page will not know it has, and will go on saying what it says. Whether Vercel counts the request at its own edge is its record and not ours; as of 26 September 2026 no one here had opened it, and we keep no copy of it. No cookie; the local storage above does that job. Some things are recorded that the list above does not name. Your browser and the network attach these to the request rather than the page sending them: the identification string your browser gives with every request; the two-letter country the network assigns your address — no city, and your address itself is never stored; and the site address you asked for. Our own server then writes its own bookkeeping about the record: the date and time of your visit, to the thousandth of a second, from our clock; which request header it took that site address from; and a number naming the record format it wrote. It also stores the domain your browser said the count was fired from, which for a normal load of this page is this site itself, and which our server records as the word “unknown” when the browser sends nothing it can read. Every row is kept in Vercel’s blob storage and not on a machine of our own, measured 26 September 2026 by reading the handler. The page sends how long it was open twice, from two different counters, and our server reads only one of the two names — so some rows carry it and some are empty (measured 26 September 2026). This page reads an outreach code from the address if one is present and keeps it in your browser, which would let us tell one reader from another — but we have sent no link for this page and hold no list of recipients for it, so as of 2 October 2026 there is no name for any code to resolve to. This page also loads a second counter of its own. It sends its own copy of the view event on every load, so one load writes two view rows and any rate measured against them reads half its true value; it also measures the time differently, counting only the seconds the page was actually in front of you where the first counter sends wall-clock time since the page opened. It also sends an event once you have had the page in front of you for ten seconds and moved the mouse, touched the screen, scrolled or pressed a key. That ten-second event also depends on the count before it: this second counter does not send it unless its own copy of the view event went out first. Its “engaged” is stricter still — it is sent only after that ten-second event has gone out, you have had the page in front of you for ninety seconds in all, and you have scrolled far enough to reach a marker we put where the explaining part of this page ends; a reader who stops short of that marker never produces one. Because both counters send an event called “engaged” under different rules, a single visit can produce two of them. The host that serves it keeps its own request logs; those are its record and not ours. Every quotation above is from the linked file at the pinned commit; the same links are listed for machine readers at /llms.txt.