Skip to main content

agentsop-bounded-loop

Universal discipline for any LM-driven loop — agent retries, plan-act-observe, multi-agent handoffs, optimiser passes, test-fix cycles. Encodes the one rule every framework documents quietly and every team relearns expensively: the LM in the loop is NEVER a reliable terminator. Termination must be provided by an explicit counter + exit predicate + stagnation signal + escalation path that live OUTSIDE the LM's control. This is a tool- level, framework-agnostic skill. It maps onto LangGraph (recursion_limit + state counter + interrupt), CrewAI (max_iter + max_rpm + human_input), Claude / OpenAI SDKs (max_iterations + tool_use_budget), DSPy (declared evaluation budget), Aider (REPL + explicit retry cap), and AutoGen (max_consecutive_auto_reply). Search keywords: infinite loop, recursion limit, recursion_limit, GraphRecursionError, max iterations, max_iter, agent stuck, agent won't stop, runaway agent, ReAct loop not terminating, agent repeating itself.

Informations de source

Dépôt
agentsope/SkillAlchemy
Dernière activité de la source
20 mai 2026 à 14:58
Langue détectée de SKILL.md
anglais
Étoiles
457
Forks
25

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
4 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
agentsop-bounded-loop
version
0.1.0
description
Universal discipline for any LM-driven loop — agent retries, plan-act-observe, multi-agent handoffs, optimiser passes, test-fix cycles. Encodes the one rule every framework documents quietly and every team relearns expensively: the LM in the loop is NEVER a reliable terminator. Termination must be provided by an explicit counter + exit predicate + stagnation signal + escalation path that live OUTSIDE the LM's control. This is a tool- level, framework-agnostic skill. It maps onto LangGraph (recursion_limit + state counter + interrupt), CrewAI (max_iter + max_rpm + human_input), Claude / OpenAI SDKs (max_iterations + tool_use_budget), DSPy (declared evaluation budget), Aider (REPL + explicit retry cap), and AutoGen (max_consecutive_auto_reply). Search keywords: infinite loop, recursion limit, recursion_limit, GraphRecursionError, max iterations, max_iter, agent stuck, agent won't stop, runaway agent, ReAct loop not terminating, agent repeating itself.
# bounded-loop · O7 > Source posture: every load-bearing claim is cited inline with a short tag > resolved against `references/R1-source-evidence.md` and > `references/R2-cross-framework.md`. Examples cite the real GitHub issues > they're distilled from. --- ## 1. 何时激活 (Activation Rules) Activate this skill when **any** of the following is true: - The task involves a workflow that contains a **cycle** — tool-call → reflect → retry, plan → act → observe → re-plan, draft → critique → revise, test → fix → re-test. - The user is hitting a framework's "loop too deep" error: `GRAPH_RECURSION_LIMIT` (LangGraph), `MaxIterationsExceeded` (LangChain `AgentExecutor`), "agent exceeded max_iter" (CrewAI), `max_turns reached` (OpenAI Agents SDK), `stop_reason="max_tokens"` mid-tool-use (Anthropic). - The user proposes "let's just raise the limit" / "set max_iter to 100" / `recursion_limit=200` — this is the canonical anti-pattern this skill exists to prevent. - The user is building a **multi-agent** system with delegation, handoff, or supervisor patterns — these are exposure-multipliers for unbounded loops (see `[gh/crewai-330]`). - The user is building an **optimiser / evaluator loop** (DSPy, AutoEval, RLHF, self-refining agent) where "stop when good enough" is the termination criterion — this is *never* sufficient on its own. - The user wants a **test-fix loop**, **self-healing code agent**, or **iterative refinement** workflow — every code-agent in production (Cursor, Aider, Devin, Claude Code) ships with an explicit step budget. Do **not** activate for: single LLM calls, one-shot RAG queries, stateless tool pipelines, or flows where the cycle is provably bounded by data (e.g., "iterate once per row in this fixed list"). --- ## 2. 核心心智模型 (Core Mental Model) **Every loop body must produce a state change that proves progress — and the proof must be checkable without calling another LM.** Read that twice. It contains four claims: 1. **The body must change state.** A no-op iteration (same input → same output) is the definition of a stuck loop. If your body might return the same value twice, the loop is already broken; the safety net just hasn't fired yet. 2. **The change must be progress, not just diff.** A retry that says "I tried again, same error" is a change but not progress. The witness has to be monotone: counter strictly increasing, error list strictly shrinking, confidence strictly rising, or a new fact added to the plan. 3. **The proof must be checkable.** Pure Python. A `dict.get("retries") < N`, not `await llm.ainvoke("are we done?")`. If you ask the LM to evaluate termination, you've recreated the problem one level up — now *that* loop needs bounding. 4. **The LM is not allowed to vote.** It can *suggest* finality (`stop_reason="end_turn"`, `final_answer` tool, etc.) but the framework must verify against the predicate before terminating. Otherwise an LM that always says "let me try once more" runs forever. ### Why the framework's default safety net is not enough Every framework ships a default cap: - LangGraph: `recursion_limit=25` `[lc-docs/errors]` - CrewAI: `Agent.max_iter=20`, `Crew.max_rpm` `[crewai-docs/agents]` - LangChain `AgentExecutor`: `max_iterations=15` (deprecated default) - OpenAI Agents: `Run.max_turns` - Anthropic Messages: `max_tokens` per call (per-call, not per-loop) These are **billing safety nets**, not control flow. The LangGraph docs say so explicitly: > "If you are not expecting your graph to go through many iterations, you > likely have a cycle. Check your logic for infinite loops." > — `[lc-docs/errors]` `https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT` And the cheatsheet adds: > "Hitting the limit typically indicates an underlying design flaw. The > recursion limit is a safety net for runaway code, not a primary control > flow mechanism." > — `[cheatsheet/gotchas]` When you raise the limit to "fix" the error, you've **moved the bug further away**, not removed it. The text-to-SQL agent in `[gh/6731]` would have hit `recursion_limit=100` after burning 5× the Databricks quota. ### The three-axis termination model A bounded loop has three independent termination axes; you need at least two firing in series: ``` ┌─── (a) success predicate met → exit success │ [loop body] ────┼─── (b) counter / budget exhausted → exit escalation │ └─── (c) stagnation detected → exit escalation ``` If you only have (a), the LM controls termination — it doesn't. If you only have (b), you'll burn the budget on N identical iterations. If you only have (c), one-shot flake will look like success. Compose all three. --- ## 3. SOP 工作流 (Standard Operating Procedure) A coder agent walks this top-down. Each step has a decision gate — answer "no" and you go back, not forward. ### Step 1 · Identify the loop body and the cycle invariant Before adding *any* bound, write down on paper: - What is the loop body? (one function / one node / one task) - What input does it read? What output does it write? - What state field MUST be different on iteration N+1 vs iteration N for this to be progress? That field is your **progress witness**. Gate: if you can't name the witness, you don't yet understand the loop well enough to bound it. Don't add a counter — go think. Common witnesses by workflow shape: | Workflow | Witness | |---|---| | Tool-call → error → retry | `last_error` text must change (or counter increments) | | Plan → act → observe | `plan_revision: int` strictly increases | | Draft → critique → revise | `critique` length shrinks OR `revision_count` increments with non-empty diff | | Test → fix → re-test | `failing_tests` set strictly shrinks | | Optimiser sweep | `best_metric` strictly improves (with patience) | | Multi-agent handoff | `task_status` transitions through a state machine, not "in_progress → in_progress → ..." | ### Step 2 · Add the iteration counter Counter discipline: - **One counter per loop**, not per agent. In multi-agent systems where agents can call each other (CrewAI delegation, LangGraph subgraphs), the counter must live in the **shared** state, not per-agent `max_iter` — that is the CrewAI ping-pong bug `[gh/crewai-330]`. - **Counter is monotonic** — `Annotated[int, operator.add]` in LangGraph, not a state replace. - **Counter is visible** — log it; surface it in traces. A counter you can't see in LangSmith / Maxim / Datadog is a counter you'll forget is there. Pseudocode (framework-agnostic): ```python def loop_body(state): new_state = do_one_iteration(state) new_state["retries"] = state.get("retries", 0) + 1 return new_state def should_continue(state) -> Literal["continue", "give_up"]: if state["retries"] >= MAX_RETRIES: return "give_up" if success_predicate(state): return "end" return "continue" ``` ### Step 3 · Add the stagnation detector The counter alone wastes (N-1) iterations on identical work. Add a progress witness comparison: ```python def should_continue(state): if state.get("last_witness") == state.get("witness"): return "give_up_stagnant" if state["retries"] >= MAX_RETRIES: return "give_up_budget" if success_predicate(state): return "end" return "continue" ``` Stagnation signals worth detecting: - Same `last_error` two iterations running. - Same `tool_calls` hash (same tool, same args) two iterations. - `plan_revision` did not increment. - `failing_tests` did not shrink (test-fix loop). When stagnation fires, **always escalate** — don't retry. ### Step 4 · Pick the LM's view of the loop state The LM must see the loop counter and the last error / last witness. If it doesn't, it will happily repeat. Concretely: - **LangGraph**: include `retries` and `last_error` in the messages passed to the LLM node — or render them into the system prompt at each iteration. - **CrewAI**: surface the previous task's failure in the next task's `context=[...]`, not in `Crew.memory` (which is muddier). - **Claude / OpenAI SDKs**: in the next user/tool-result message, include `"Attempt {n} of {N}. Previous error: {err}. If you cannot fix it on this attempt, return final_answer with status=failed."` Without this, the LM thinks it's on iteration 1 forever. The framework's counter is in your code; the *behavioural* counter must be in the prompt. ### Step 5 · Build the escalation branch *before* you remove the safety net The framework's safety net exists for a reason — runaway billing. Don't disable it. Instead, build the **graceful give-up** that catches the counter/stagnation exit: - LangGraph: a `give_up` node that calls `interrupt({"reason": ...})`, preserving the full state for a human or outer agent to inspect. - CrewAI: a fallback `Task` with `human_input=True` that fires when the main task fails the validation in `expected_output`. - Claude SDK: a `human_escalation` tool that the model is *forced* to call when `attempt == N`. - OpenAI Agents: handle `Run.status == "incomplete"` / `incomplete_reason == "max_turns"` in the caller and surface to user. Rule of thumb: **a loop without a give-up branch is a loop that fails to a stack trace**. That's not graceful. ### Step 6 · Layer the outer safety bound Even with counter + witness + escalation, each individual iteration can be expensive (one tool call doing a 200k-token web search). Add: - **Token budget**: sum input + output tokens across iterations; cap. - **Wall-clock timeout**: `asyncio.wait_for(loop, timeout=T)` at the outermost caller. - **Rate limit**: CrewAI `max_rpm`, OpenAI tier limits, Anthropic `requests_per_minute`. Hit these *before* you hit the model's rate-limit error which adds backoff + more retries. These are not redundant with the counter — they're orthogonal axes. A 3-iteration loop where one iteration runs for 4 hours still wedges your system. ### Step 7 · Test the bound Write a regression test that *injects a permanent failure* and asserts: - The loop exits within N iterations (counter works). - The loop exits *before* N if the same error recurs (stagnation works). - The final state is captured in the escalation branch (escalation works). - The trace shows the counter visible (observability works). This is the same shape as `[gh/6731]`'s recommended fix: "Add a regression test that injects a permanent SQL error and asserts the graph terminates within 3 iterations." Steal that pattern. --- ## 4. 操作模型 (Operation Models) Each operation is a primitive a coder agent can invoke. Format: **Trigger → Action → Output → Evidence**. ### OP-1 · Retry counter in state (the foundational operation) - **Trigger**: Any cyclic LM workflow exists — even one cycle. - **Action**: Add a typed integer field to the workflow's persistent state with a monotonic semantics (LangGraph: `Annotated[int, operator.add]`; CrewAI: shared dict in `Crew` context or `Flow` state; Claude SDK: app variable). Increment inside the loop body; check in the exit predicate. - **Output**: A deterministic upper bound on iterations regardless of LM behaviour. - **Evidence**: `[gh/6731]` (text-to-SQL fix), `[lc-docs/errors]` ("explicit termination conditions are the right answer"). ### OP-2 · Stagnation detection via progress witness - **Trigger**: The counter alone keeps firing — you're wasting N iterations on identical retries. - **Action**: Store the previous iteration's distinguishing artifact (`last_error`, `last_tool_call_hash`, `last_witness`) in state. In the exit predicate, compare current vs previous *before* checking the counter. Same → exit `stagnant`, don't increment. - **Output**: Fast-fail on truly stuck loops; reserves the counter for cases where iterations *are* progressing slowly. - **Evidence**: `[gh/6731]` (LLM was retrying identical query 20 times — stagnation would have fired after 1). ### OP-3 · Human / outer-loop escalation - **Trigger**: Counter exhausted OR stagnation detected. - **Action**: Route to a designated escalation node that *does not crash*. LangGraph: `give_up` node → `interrupt({"reason", "state"})`. CrewAI: fallback `Task(human_input=True)` or Flow `@listen("failed")` branch. Claude SDK: `human_escalation` tool injection. OpenAI Agents: catch `incomplete_reason == "max_turns"` in caller. - **Output**: A clean give-up branch carrying enough state for a human or outer agent to diagnose. - **Evidence**: `[lc-blog/interrupt]` four-pattern table; Aider's REPL return-to-human on failed test. ### OP-4 · Token & wall-clock budget (orthogonal safety net) - **Trigger**: Iterations are themselves expensive (long context, web search, code execution). - **Action**: Track cumulative tokens in state; enforce wall-clock `asyncio.wait_for` at outer caller; enforce per-model `requests_per_minute`. Any one tripping → escalation (OP-3). - **Output**: Defense-in-depth — neither "3 cheap iterations" nor "1 expensive iteration" can run away. - **Evidence**: Anthropic Computer Use "step budget"; CrewAI `max_rpm`; OpenAI `max_completion_tokens` / `max_prompt_tokens` on `Run`. ### OP-5 · Progress witness declared up front - **Trigger**: Designing a new cyclic flow. - **Action**: Identify the state field that MUST change between iterations to prove progress. Declare it as a typed field. The exit predicate verifies it changed; the LM's prompt is told to set it. - **Output**: A loop body that is structurally incapable of being a no-op. - **Evidence**: `[cheatsheet/gotchas]` "Treat each node like a pure function — return a partial state update"; LangChain best practices. ### OP-6 · Refuse-to-raise-the-net (diagnostic) - **Trigger**: User asks "just raise `recursion_limit` / `max_iter`." - **Action**: Refuse and diagnose. Walk: (a) is there a state counter? (b) is there a witness? (c) does the LM see the previous error? Add what's missing. Leave the framework default in place — it's a circuit breaker, not a control knob. - **Output**: A diagnostic conversation that ends with OP-1+OP-2 instead of papering over the failure. - **Evidence**: `[gh/6731]` maintainer marked "not planned" — i.e., this is *by design*. `[cheatsheet/gotchas]` "indicates an underlying design flaw." --- ## 5. 困境决策案例 (Dilemma Cases) ### Case 1 · "Text-to-SQL agent loops 20× to GRAPH_RECURSION_LIMIT" - **Source**: `[gh/6731]` (https://github.com/langchain-ai/langgraph/issues/6731), maintainer labelled "not planned." - **困境**: A team built a text-to-SQL agent on LangGraph 1.0.6. When the Databricks query returned an error, the agent retried the same broken SQL 20 times until the default `recursion_limit=25` fired. It had worked on 0.6.x; the upgrade exposed the missing exit condition. - **约束**: - Can't pin to 0.6.x — security fixes only in 1.x. - Maintainer won't ship a fix — explicitly "not planned." - Databricks quota is bleeding; business needs the agent live. - **决策步骤**: 1. **Reject** "just raise `recursion_limit` to 100." That makes the bleeding worse and confirms the cheatsheet's diagnosis `[cheatsheet/gotchas]`. 2. Add state counter (OP-1): ```python class S(TypedDict):
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub