@@ -1661,3 +1661,72 @@ def test_the_incident_example_state_merges_parallel_writers():
16611661 result = loop .run ("triage, patch and verify at once" , IncidentState ())
16621662 assert result .stop .value == "goal_met"
16631663 assert sorted (result .state .notes ) == ["patch ran" , "triage ran" , "verify ran" ]
1664+
1665+
1666+ def test_an_empty_proposal_against_an_unmet_goal_gets_one_nudge ():
1667+ """Empty plan, goal check unsatisfied: the contradiction is named once.
1668+
1669+ The planner is told the goal check is not met and asked to either propose
1670+ the remaining work or repeat the empty proposal. Here it proposes the
1671+ work, and the run finishes on the goal — where before the nudge existed,
1672+ round 1's empty reply ended the run as `no_further_work` with the goal
1673+ never mentioned to the model.
1674+ """
1675+ loop , model , bodies = build_loop (
1676+ [NOTHING_MORE , plan (("write" , "summarise" ))], goal_reached = goal_is_done
1677+ )
1678+
1679+ result = loop .run ("summarise the findings" , LoopState ())
1680+
1681+ assert result .stop is LoopStop .GOAL_MET
1682+ assert bodies .ran == ["write" ]
1683+ assert len (result .rounds ) == 2
1684+ assert not result .rounds [0 ].executed and result .rounds [0 ].admitted
1685+ assert any (
1686+ "goal check is not yet satisfied" in str (message .content )
1687+ for message in model .calls [1 ]
1688+ )
1689+
1690+
1691+ def test_a_second_empty_proposal_is_believed ():
1692+ """The nudge is one round, not a counter: a repeat empty plan is an answer."""
1693+ loop , model , bodies = build_loop (
1694+ [NOTHING_MORE , NOTHING_MORE ], goal_reached = goal_is_done
1695+ )
1696+
1697+ result = loop .run ("summarise the findings" , LoopState ())
1698+
1699+ assert result .stop is LoopStop .NO_FURTHER_WORK
1700+ assert "confirmed no further work" in result .detail
1701+ assert bodies .ran == []
1702+ assert len (result .rounds ) == 2
1703+ assert model .call_count == 2
1704+
1705+
1706+ def test_an_unusable_reply_after_the_nudge_confirms_no_further_work ():
1707+ """A planner with nothing left to say after the nudge is not a failure.
1708+
1709+ The scripted stand-ins end their reply lists with an empty proposal; the
1710+ nudge asks one more question than the script answers. Exhaustion there
1711+ must read as the confirmation it is — never as `planning_failed` burning
1712+ the failure allowance on a planner that already said "nothing more".
1713+ """
1714+ loop , model , bodies = build_loop ([NOTHING_MORE ], goal_reached = goal_is_done )
1715+
1716+ result = loop .run ("summarise the findings" , LoopState ())
1717+
1718+ assert result .stop is LoopStop .NO_FURTHER_WORK
1719+ assert "nothing usable" in result .detail
1720+ assert bodies .ran == []
1721+ assert result .rounds [- 1 ].planner_error
1722+
1723+
1724+ def test_an_empty_proposal_with_no_goal_check_stops_without_a_nudge ():
1725+ """No goal check means nothing to contradict: one round, one clean stop."""
1726+ loop , model , bodies = build_loop ([NOTHING_MORE ])
1727+
1728+ result = loop .run ("nothing needs doing" , LoopState ())
1729+
1730+ assert result .stop is LoopStop .NO_FURTHER_WORK
1731+ assert len (result .rounds ) == 1
1732+ assert model .call_count == 1
0 commit comments