fix(loop): graceful exit on max iterations with wrap-up nudge + forced text-only#148
Conversation
…d text-only AgentLoop could exhaust all 50 iterations when the LLM kept calling tools, resulting in status="failed" with no useful output. Mirror the swarm worker's proven approach: inject a wrap-up nudge at 80% of iteration budget and drop tool definitions on the last iteration to force a text response.
Follow-up to #148. The wrap-up nudge fired at iteration == wrap_up_at unconditionally, which on tiny budgets (max_iter<=2 -> wrap_up_at==1) injected a '[SYSTEM] wrap up' user message on the very first iteration, displacing the active research-goal context as the most recent user message and breaking test_agent_goal_context. It also conflicted with goal-continuation by telling the agent to stop early. Now the nudge only fires for 1 < iteration < max_iterations: never on the first iteration, and never on the last (the forced text-only path already guarantees a final answer there).
|
Merged — thanks @toanalien! 🙏 Mirroring the swarm worker's wrap-up + forced-text-only pattern into Heads-up: I pushed a small follow-up on top ( |
…d text-only (HKUDS#148) AgentLoop could exhaust all 50 iterations when the LLM kept calling tools, resulting in status="failed" with no useful output. Mirror the swarm worker's proven approach: inject a wrap-up nudge at 80% of iteration budget and drop tool definitions on the last iteration to force a text response.
Follow-up to HKUDS#148. The wrap-up nudge fired at iteration == wrap_up_at unconditionally, which on tiny budgets (max_iter<=2 -> wrap_up_at==1) injected a '[SYSTEM] wrap up' user message on the very first iteration, displacing the active research-goal context as the most recent user message and breaking test_agent_goal_context. It also conflicted with goal-continuation by telling the agent to stop early. Now the nudge only fires for 1 < iteration < max_iterations: never on the first iteration, and never on the last (the forced text-only path already guarantees a final answer there).
Summary
AgentLoopcould exhaust all 50 iterations when the LLM kept calling tools endlessly, resulting instatus="failed"with no useful output returned to the user.test_force_text_only_on_last_iterationvalidating that an LLM which always returns tool calls now producesstatus="success"instead ofstatus="failed".Changes
agent/src/agent/loop.pywrap_up_at = max(1, int(self.max_iterations * 0.8))before the main loop[SYSTEM]user message nudging the LLM to stop calling tools and produce a final answertools=Nonetostream_chat— the LLM cannot request tool calls and must produce text output, guaranteeing a final answeragent/tests/test_agent_loop_terminal_state.py_StubLLMAlwaysToolCalls— LLM stub that returnscompacttool calls untiltools=Noneforces texttest_force_text_only_on_last_iteration— verifies the forced text-only last iteration producesstatus="success"with contentHow it works
The swarm worker (
agent/src/swarm/worker.py) already had both mechanisms (lines 363-426). This PR brings the same pattern toAgentLoop:Test plan
test_agent_loop_terminal_state.pypass unchangedtest_force_text_only_on_last_iterationpassesruff checkpassespython -m py_compilepasses