Skip to content
← All codeCode sample

why-agents-loop-forever

A Strands Agents planner and executor with a deterministic review step that checks system state and reports only to the planner, so an agent stops instead of repeating a call that cannot change anything

View on GitHub

ServiceEnv is a service that is down because its db_url points at a decommissioned database. Its restart_service tool always returns "restart ok" and changes nothing the goal can see, which is how a naive agent ends up calling it forever.

Orchestrator.run() runs rounds of planner, executor and review, under a max_rounds budget. The planner is a Strands Agent with no tools that writes a numbered plan. The executor is a fresh Strands Agent each round, bounded by a per-call Limits(turns, total_tokens). Its NoProgressGuard hook runs on BeforeToolCallEvent. It cancels a tool call repeated from an unchanged state and ends the run. GoalCheck.review() compares the state before and after the round and returns pass, fail or stalled; its findings go to the planner and never to the executor.

The 13 tests reproduce the restart loop, a transcript that claims success the state does not show, the turn and token limits, and the round budget. They also check that the executor's second round starts from the plan alone. The tests run offline with a scripted fake model and never call a provider.

Bash
git clone https://github.com/tensorupai/tensorup-examplescd tensorup-examples/why-agents-loop-forever && uv sync && uv run pytest

To run it live, put OPENAI_API_KEY or ANTHROPIC_API_KEY in .env, then run TENSORUP_LIVE=1 uv run python -m loopstop. TENSORUP_PROVIDER picks openai (default) or anthropic, and TENSORUP_MODEL overrides the default model.

To use it in your own agent, replace ServiceEnv.snapshot() with the fields your goal is defined on, and write GoalCheck.unmet() as a check over them. Keep the guard's fingerprint to those same fields, and pass limits on every executor call.