Maximum number of messages 10 reached, current message count: 10
That is Microsoft AutoGen’s AgentChat line, and it is the only string on this site that is not an error. Nothing raises it. MaxMessageTermination builds that sentence as the content of a StopMessage and returns it, and a team puts it on the stop_reason field of a TaskResult that it yields as a normal, successful result. The 10 is not a default — there is no default — it is whatever number you passed in. If your code decides what to do next by catching exceptions, this ceiling is invisible to it.
In ten seconds. This is a termination condition, not an error. It returns, so there is no except block to put anything in: the run comes back looking like it succeeded. The thing to read is TaskResult.stop_reason — if it starts with Maximum number of messages, the work was cut off, not finished.
The ceiling is max_messages, and it is a required argument with no default value. By default the counter counts only chat messages and skips agent events, so the number is not the number of lines you saw go past.
From _terminations.py, lines 83–92 as fetched. This is the whole of MaxMessageTermination.__call__:
async def __call__(self, messages: Sequence[BaseAgentEvent | BaseChatMessage]) -> StopMessage | None:
if self.terminated:
raise TerminatedException("Termination condition has already been reached")
self._message_count += len([m for m in messages if self._include_agent_event or isinstance(m, BaseChatMessage)])
if self._message_count >= self._max_messages:
return StopMessage(
content=f"Maximum number of messages {self._max_messages} reached, current message count: {self._message_count}",
source="MaxMessageTermination",
)
return None
Two things in that block are the whole page. The first is return StopMessage( on line 88, where every other framework on this site has a raise. The second is that the number printed twice in the message comes from two different variables: self._max_messages, the cap you set, and self._message_count, the count as it stands. They are not always equal — see below.
The constructor, line 74 of the same file:
def __init__(self, max_messages: int, include_agent_event: bool = False) -> None:
max_messages carries no default. Python will refuse to construct the condition without it, so unlike every other ceiling on this site there is no number quietly waiting for you in a constant somewhere — whatever appears in the message is a number you chose. The 10 in the phrase at the top of this page is the value AutoGen’s own docstring example uses, in _termination.py at line 35 of the file as fetched: MaxMessageTermination(10) | TextMentionTermination("TERMINATE").
What gets counted is decided by the comprehension on line 86. Each call adds the number of messages in that batch which satisfy self._include_agent_event or isinstance(m, BaseChatMessage). With the default include_agent_event=False, only BaseChatMessage instances count, and anything that is a BaseAgentEvent — tool-call events, streaming chunks, the bookkeeping a team emits as it moves between participants — is counted as zero. So the cap is not “things I saw in the transcript” and not “model calls” either: it is chat messages exchanged, and one agent turn can be worth more than one of them.
The counter also increments before it compares, and compares with >=. One call can add several messages at once, so self._message_count can overshoot self._max_messages rather than landing on it. That is why the message prints both numbers: a message reading Maximum number of messages 10 reached, current message count: 12 is not a bug, it is a batch of three messages arriving while the count stood at nine.
Line 85 does raise something, but not when the cap is hit. It raises when the condition is called again after it has already terminated and has not been reset:
if self.terminated:
raise TerminatedException("Termination condition has already been reached")
So the two outcomes are: first contact with the ceiling returns a value, and any call after that throws. That matters more than it looks, because of what TerminatedException is. From base/_termination.py, line 12 of the file as fetched, in its entirety:
class TerminatedException(BaseException): ...
It subclasses BaseException, not Exception. A try / except Exception around your run will not catch it — it goes past the same way KeyboardInterrupt does. If you have a broad handler that logs and continues, and you are wondering why this one gets through it, that is the reason, and it is a one-word reason on line 12.
Two more details from that file, both at the same commit, both worth knowing if you combine conditions with & or |. AndTerminationCondition.__call__ raises TerminatedException("Termination condition has already been reached.") at line 107 — with a full stop at the end of the sentence, where the eleven raise sites in _terminations.py have none. And OrTerminationCondition.__call__, line 158, raises a different class altogether:
if self.terminated:
raise RuntimeError("Termination condition has already been reached")
A RuntimeError, which is an Exception and will be caught by a broad handler. So in these two files at this commit, the same English sentence reaches your code as three distinguishable things depending on how the condition was assembled: a BaseException from a bare condition, a BaseException with a trailing period from an &, and an ordinary Exception from a |. We are describing only the two files named in the provenance section below; we did not read the rest of the package and make no claim about it.
The | combinator also changes the string itself. Lines 161–164 of that file join the contents of every condition that fired with ", ", so if your message ceiling and a text-mention condition trip on the same batch, the sentence at the top of this page arrives glued to another one, and so does the source.
From teams/_group_chat/_base_group_chat.py, lines 553–556 and 564 as fetched:
if message.error is not None:
raise RuntimeError(str(message.error))
stop_reason = message.message.content
break
yield TaskResult(messages=output_messages, stop_reason=stop_reason)
That is the whole path. A real failure inside the team becomes a RuntimeError on line 554. A termination condition firing is not a failure, so it takes line 555 instead: the StopMessage’s content — the sentence at the top of this page — is copied onto a local variable, the loop breaks, and the team yields a TaskResult carrying every message it collected plus that sentence as stop_reason. Your code is handed a successful result with a string field explaining that it is not finished. Nothing in the control flow forces you to read that field.
The same file carries a separate knob on line 74 of its constructor:
max_turns: int | None = None,
Its default is None — a team has no turn ceiling unless you give it one. It is stored on line 88 and handed to the group-chat manager on line 225. What message that cap produces is decided in the manager, which is a file we did not fetch, so this page says nothing about it.
The number is a budget for conversation, not for progress. Nothing in the three files above inspects whether the work is going anywhere: the counter only ever goes up, by one per chat message, and the condition only ever compares it against your constant. A group of agents that is circling — asking each other for the same clarification, or handing a task back and forth — consumes the budget at exactly the same rate as a group that is finishing things, and the condition cannot tell the difference, because the only thing it is given is the count.
So doubling max_messages buys exactly twice the messages. If the run was going to finish in thirty messages and you set twenty, the raise fixes it once and for the one brief. If the run was circling, the raise buys you twice the circling and the same sentence at the end, with two larger numbers in it. The sentence prints both numbers precisely so you can tell those apart: the gap between max_messages and current message count tells you how fast the last batch was arriving, and nothing at all about whether the task was close to done.
Because this ceiling returns, the usual advice — put your save in the except block — has nothing to attach to. There is no except block. The two things to do instead both follow from the code above:
There is a written guide: the messages-per-item arithmetic above as a formula you can run against a brief before you launch it, how to tell a result that arrived as a success from one that was finished, 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.
Three files, for this page, all HTTP 200, all fetched 2 October 2026, all pinned at the same commit — 027ecf0a379bcc1d09956d46d12d44a3ad9cee14, the head of main in microsoft/autogen at the time of the fetch: python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py, 22,531 bytes; python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py, 7,523 bytes; and python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py, 38,510 bytes. Every string, line and line number quoted above is from those three fetches, and every claim about what does and does not happen is scoped to those three files at that commit only. Line numbers are line numbers in the files as fetched and will move when the files do.
The short version: Maximum number of messages N reached, current message count: N is not an exception. MaxMessageTermination returns it as the content of a StopMessage, and a team copies that content onto TaskResult.stop_reason and yields the result as a success. max_messages has no default — it is a required argument — and the counter adds one per BaseChatMessage only, skipping agent events unless include_agent_event=True. The exception that does exist here, TerminatedException, fires on the next call after termination and subclasses BaseException, so except Exception misses it; assembled with | instead, the same sentence arrives as an ordinary RuntimeError. Read stop_reason before you read the result, persist each message as the stream yields it, and batch the items so one message covers many.
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.