Skip to content

cycle_detection: architect + developer graphs have no explicit recursion_limit / max_iterations guard #6

Description

@hatyibei

Summary

Both agent/architect/graph.py and agent/developer/graph.py define LangGraph StateGraph workflows that contain conditional cycles between tool-calling nodes and routing nodes, but workflow.compile() is invoked without an explicit recursion_limit argument. As a result, both agents fall back to LangGraph's default ceiling (25 super-steps) — when the LLM's tool-selection logic stalls in a loop, the run aborts with GraphRecursionError rather than terminating gracefully with a partial result.

This was found by shingan, a workflow static-analyzer for agent graphs. It flagged 4 sites at confidence 0.9:

File Node in cycle Cycle topology
agent/architect/graph.py conduct_research conduct_research → tools → conduct_research
agent/architect/graph.py come_up_with_research_next_step come_up_with_research_next_step → check_research_step → conduct_research → … → come_up_with_research_next_step
agent/developer/graph.py get_clear_implementation_plan_for_atomic_task … → research_tool_node → get_clear_implementation_plan_for_atomic_task
agent/developer/graph.py prepare_for_implementation prepare_for_implementation → … → prepare_for_implementation (via developer routing)

Each cycle has a conditional exit branch — so they're not unbounded in principle — but the bound is implicit in the routing-function's contingent behavior, not declared at the graph layer.

Why this matters

For a research/SWE agent operating on real codebases, a 25-step ceiling is often too tight (large repos can need 30-40 tool calls just to map files + reason). The current symptom on the user side is a hard GraphRecursionError with no partial output — annoying to debug, and the fix (compile(recursion_limit=N) or a RunnableConfig override) isn't discoverable from the code.

Two related risks:

  1. No max-iteration safety net if a routing function regresses into an unbounded oscillation (LLM keeps selecting toolsconduct_researchtools). Production logs would show a hung agent for ~25 steps before crashing.
  2. No telemetry on iteration count. Even if the agent succeeds, there's no record of "this query needed 18 iterations" to feed back into prompt/router tuning.

Repro

git clone https://github.com/langtalks/swe-agent
cd swe-agent
pip install langgraph
python -c "
from agent.architect.graph import swe_architect
print(swe_architect.get_graph().draw_ascii())
# Note: no recursion_limit visible in the compiled graph config.
print(swe_architect.config)  # → {'tags': ['research-agent-v3']}, no recursion_limit
"

Running on a sufficiently complex codebase (~50+ files) will hit the default ceiling on the architect's research loop before producing a plan.

Suggested fix (3 options, in order of effort)

Option 1 — Lowest effort: explicit recursion_limit at compile time.

# agent/architect/graph.py
swe_architect = workflow.compile().with_config({
    "tags": ["research-agent-v3"],
    "recursion_limit": 50,  # research can take more steps than the default 25
})

# agent/developer/graph.py
swe_developer = workflow.compile().with_config({
    "tags": ["developer-agent-v3"],
    "recursion_limit": 50,
})

Pros: 2-line change, immediately removes the hard crash on medium repos.
Cons: still no per-cycle bound; a runaway router can still burn 50 steps.

Option 2 — Add an iteration counter to state, route to a terminal on overflow.

In agent/architect/state.py (and equivalent for developer):

class ArchitectState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    research_iterations: int  # default 0
    # ...

In agent/architect/graph.py, modify the router (the function feeding add_conditional_edges after check_research_step):

MAX_RESEARCH_ITERATIONS = 20

def _should_continue_research(state: ArchitectState) -> Literal["conduct_research", "extract_implementation_plan"]:
    if state.get("research_iterations", 0) >= MAX_RESEARCH_ITERATIONS:
        return "extract_implementation_plan"  # graceful fallback
    # ... existing logic

And increment the counter in come_up_with_research_next_step. Pros: graceful degradation, no hard crash. Cons: ~15 lines of changes per agent.

Option 3 — Documentation only.

Add a ## Configuration section in README.md explaining that for large codebases, callers should pass config={"recursion_limit": 50} when invoking the agent (e.g. swe_architect.invoke(input, config={"recursion_limit": 50})). Lowest effort, but doesn't change default behavior.


Happy to send a PR for Option 1 (1-2 lines, no test changes) or Option 2 (with regression tests for the cap behavior) — whichever you'd prefer. If neither, Option 3's README note alone would still help discoverability.

Same pattern appeared in two other LangGraph-based researchers I scanned, where the maintainers chose Option 1 in one case and Option 2 in the other:

  • assafelovic/gpt-researcher#1766
  • langchain-ai/open_deep_research#269

Worth picking whichever is most consistent with how swe-agent is intended to be operated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions