Once you connect agents to other agents, a new class of failure shows up that single-agent systems never see: agents tossing work back and forth without finishing, supervisors that can't tell whether their workers are actually making progress, and parallel branches that quietly overwrite each other's results. This chapter walks through each failure mode in plain language, then catalogs the iteration caps, plan-tracking surfaces, and state-merging primitives the five frameworks expose for these conditions.
Two agents end up tossing the same piece of work back and forth instead of finishing it. Each one decides the other is better positioned to handle the next step, so neither ever commits to closing the task. None of the frameworks recognise this as a cycle — they only notice that some loop has run too many times and trip a global iteration counter.
Picture a researcher agent and a writer agent collaborating on a one-page report. The writer drafts something, decides it lacks facts, and hands the draft back to the researcher. The researcher pulls more facts and hands them back. The writer drafts again, decides the structure could be tighter, and asks the researcher for a "better-sourced" version. There's no shared sense of "done" — each agent's prompt says "delegate when you need help", and each one sees the other as the one who should make the final call. With no fixed plan and no progress tracking, this can repeat indefinitely.
What the frameworks actually detect: not the cycle itself, but the symptom. The iteration cap fires after N total rounds regardless of why the loop is long — productive long-running work and pointless ping-pong look identical to the counter. The cap exists as a budget guard, not a cycle detector.
| Framework | Cycle-detection lever | Source |
|---|---|---|
| LangChain | Classic AgentExecutor.max_iterations caps round-trips of the outer agent (default 15). For agents-as-tools, only the outer cap counts inner round-trips. |
libs/langchain/langchain_classic/agents/agent.py:1023 |
| LangGraph | recursion_limit on the runnable config; on reach, raises GraphRecursionError. |
libs/langgraph/langgraph/pregel/main.py:2974 |
| CrewAI | Agent.max_iter (default 25) caps per-task iterations. In hierarchical Process the manager's iterations cap delegations. |
lib/crewai/.../agents/agent_builder/base_agent.py:251 |
| Google ADK | No global cap on transfer chains. LoopAgent.max_iterations caps loops at the LoopAgent boundary; AutoFlow.agent_transfer does not track cycles. |
src/google/adk/agents/loop_agent.py:62 |
| Semantic Kernel | AgentGroupChat's TerminationStrategy.maximum_iterations: base default 99; DefaultTerminationStrategy sets it to 5. |
strategies/termination/{termination_strategy,default_termination_strategy}.py |
In a supervisor pattern, one agent decides which worker handles each step. If the supervisor's prompt says "keep going until the task is complete" but the supervisor can't tell whether work is actually moving forward, it will keep dispatching similar-looking sub-tasks. Each round produces a slightly different output, so to the supervisor it feels like progress — but the goal never gets closer.
A "research lead" agent supervises a team of specialist sub-agents. The user asks: "Find five competitor pricing models." The supervisor dispatches a sub-agent and gets back four pricing models. It dispatches again — "find one more" — and gets the same four with a new fifth entry. It dispatches again to "verify the fifth", and this time receives three of the originals plus a different fifth. Every round produces new content, which to the supervisor reads as forward motion. But the actual deliverable — a stable list of five — never settles.
This is exactly what plan-progression tracking is designed to catch. Instead of just
counting rounds, the orchestrator records each step's intended outcome, compares it to
what the step actually produced, and flags "no real progress" so a re-plan can fire. SK's
StandardMagenticManager is the only framework in the catalog that ships
this surface; the other four rely on the iteration cap to eventually halt the spin.
| Surface | Where | Source |
|---|---|---|
| Plan-progression tracking | SK StandardMagenticManager uses a ProgressLedger to track whether each step is making progress; plan / replan methods adjust the plan when stuck. |
orchestration/magentic.py:92, 144, 158, 205 |
| Time budget | LangChain AgentExecutor.max_execution_time; everywhere else, time-bounding sits outside the framework. |
libs/langchain/langchain_classic/agents/agent.py:1028 |
| Per-call cap | LangChain v1 ModelCallLimitMiddleware caps total model calls per thread and per run. |
libs/langchain_v1/langchain/agents/middleware/model_call_limit.py |
| Iteration cap | The same iteration limits from the previous section act as a final guard. | — |
When two agents run in parallel, they share a workspace — a dict-like object the framework keeps around for the run. If both agents write to the same field, the framework has to decide how to combine their outputs. Each of the five frameworks makes this decision differently, and in some defaults the answer is "silently throw one of them away."
Imagine a planner kicks off two sub-agents to work in parallel: one researches Vendor A,
the other researches Vendor B. Both write their findings into a shared notes
field on the run state. Without any merge logic, whichever sub-agent finishes second
overwrites the first — the planner reads notes, sees only Vendor B's
research, and continues. There's no error and no warning. The user gets an answer that
quietly ignored half the work.
Each framework provides a different primitive for handling this. LangGraph requires you
to declare a reducer per state channel — a small function that says "when there's
a new value, here's how to combine it with the old one" (e.g. add_messages
appends to a list instead of replacing it). ADK's ParallelAgent doesn't fan
into a shared field at all; sub-agents append events to a single session log, and the
ordering between branches is whatever order the events arrive in. SK's
ConcurrentOrchestration sidesteps the merge entirely by giving every parallel
agent its own private input copy and explicitly aggregating the results at the end. CrewAI
and classic LangChain don't expose true parallel state — execution is serialised, so the
problem doesn't arise.
| Framework | Merge / aggregation semantics | Source |
|---|---|---|
| LangChain | Classic AgentExecutor is single-threaded — no parallel branches. |
— |
| LangGraph | Each channel declares a reducer (e.g. add_messages for the messages channel). Without an explicit reducer, the channel is last-writer-wins. |
libs/langgraph/langgraph/graph/message.py · add_messages |
| CrewAI | Sequential and hierarchical Process serialise tasks. Task.async_execution=True opts a task into asynchronous execution; outputs propagate as context to the next task. |
lib/crewai/.../task.py · async_execution |
| Google ADK | ParallelAgent runs sub-agents concurrently against the same Session. The Session is an append-only event log; event ordering between branches is nondeterministic. |
src/google/adk/agents/parallel_agent.py:150 |
| Semantic Kernel | ConcurrentOrchestration dispatches each agent against its own copy of the input; the orchestration aggregates results. |
orchestration/concurrent.py:131 |
All three failures above eventually get caught by the same blunt safety net: an iteration counter that raises after the loop has run too many times. None of these caps know whether progress is being made — they just know the count. This section gathers every cap the five frameworks expose, with their defaults; the values vary by three orders of magnitude.
| Framework | Limit | Default | Source |
|---|---|---|---|
| LangChain | AgentExecutor.max_iterations |
15 | agent.py:1023 |
| LangChain | AgentExecutor.max_execution_time |
None |
agent.py:1028 |
| LangGraph | recursion_limit (on RunnableConfig) |
10,007 (env-overridable via LANGGRAPH_DEFAULT_RECURSION_LIMIT) |
_internal/_config.py:31 |
| CrewAI | Agent.max_iter |
25 | agents/agent_builder/base_agent.py:251 |
| CrewAI | Agent.max_retry_limit (counts tool / parse retries) |
2 | agent/core.py:233 |
| Google ADK | LoopAgent.max_iterations |
None (must be set explicitly) |
agents/loop_agent.py:62 |
| Semantic Kernel | TerminationStrategy.maximum_iterations (base) |
99 | strategies/termination/termination_strategy.py:22 |
| Semantic Kernel | DefaultTerminationStrategy.maximum_iterations |
5 | strategies/termination/default_termination_strategy.py:19 |
| Semantic Kernel | FunctionChoiceBehavior.Auto.maximum_auto_invoke_attempts |
5 | connectors/ai/function_choice_behavior.py:18 |
Source-grounded observations on multi-agent failure surfaces.
LangChain max_iterations=15, LangGraph recursion_limit=10,007,
CrewAI max_iter=25, ADK LoopAgent.max_iterations=None (explicit
set required), SK DefaultTerminationStrategy.maximum_iterations=5. Spread
spans three orders of magnitude.
StandardMagenticManager in orchestration/magentic.py:205 uses
a ProgressLedger (line 92) and plan / replan
methods (lines 144, 158) to detect when a step isn't making progress. No equivalent
ships in LangChain, LangGraph, CrewAI, or ADK.
LangGraph: per-channel reducer (e.g. add_messages); without a reducer,
last-writer-wins. ADK: shared append-only Session; event ordering between
parallel branches is nondeterministic. SK: each agent gets its own input copy; the
orchestration aggregates. CrewAI: serial by default. LangChain classic: single-threaded.
recursion_limit is env-overridable
libs/langgraph/langgraph/_internal/_config.py:31 defines
DEFAULT_RECURSION_LIMIT = int(getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "10007")).
The 10,007 baseline can be lowered globally via the environment without touching call
sites.
TerminationStrategy.maximum_iterations (base default 99) on
AgentGroupChat bounds the conversation; the orthogonal
FunctionChoiceBehavior.Auto.maximum_auto_invoke_attempts (default 5) bounds
the auto-function-call loop within a single agent turn.
AutoFlow agent transfer has no global cycle cap
flows/llm_flows/agent_transfer.py issues transfer_to_agent
actions; the runner switches active agent without an integrated cycle counter. The only
per-shape cap is LoopAgent.max_iterations on the explicit
LoopAgent wrapper.