Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions clearwing/sourcehunt/hunter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1647,13 +1647,25 @@ async def arun(self) -> HunterRunResult:
if not synthesis_injected:
near_budget = self.budget_usd > 0 and total_cost_usd >= self.budget_usd * 0.75
near_steps = step >= self.max_steps - 1
if near_budget or near_steps:
near_stall = (
self.max_steps_without_progress > 0
and steps_since_progress >= self.max_steps_without_progress
)
if near_budget or near_steps or near_stall:
synthesis_injected = True
synthesis_prompt_added = True
if near_stall and not (near_budget or near_steps):
synthesis_reason = (
"Your investigation has made no durable progress for several "
"turns and will stop after this recovery turn unless you conclude "
"it now."
)
else:
synthesis_reason = "You are approaching the end of your budget."
messages.append(
ChatMessage(
"user",
"You are approaching the end of your budget. Synthesize your final "
f"{synthesis_reason} Synthesize your final "
"findings now. For each lead you investigated, either record_finding "
"if it is source-backed, or discard it only if the source affirmatively "
"disproves it. Do not drop leads merely because you ran out of time "
Expand Down
26 changes: 26 additions & 0 deletions tests/test_deep_agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,32 @@ async def test_hunter_stalls_when_no_progress():
assert llm.achat.call_count < 20 # stops well short of max_steps


@pytest.mark.asyncio
async def test_early_stall_delivers_final_synthesis_prompt_and_can_complete():
hunter, llm = _make_hunter(agent_mode="deep", max_steps=500, budget_usd=0.0)
hunter.max_steps_without_progress = 3
llm.achat.side_effect = [
FakeResponse(
text="still investigating",
tool_calls_list=[_make_tool_call("think", {"notes": "first pass"})],
),
FakeResponse(
text="still investigating",
tool_calls_list=[_make_tool_call("think", {"notes": "second pass"})],
),
FakeResponse(text="Investigation complete; no finding was recorded."),
]

with patch("clearwing.sourcehunt.hunter.HunterTrajectoryLogger") as mock_traj:
mock_traj.for_hunter.return_value = MagicMock()
result = await hunter.arun()

assert result.stop_reason == "completed"
assert llm.achat.call_count == 3
recovery_messages = llm.achat.call_args.kwargs["messages"]
assert any("made no durable progress" in message.content for message in recovery_messages)


@pytest.mark.asyncio
async def test_late_stall_delivers_final_synthesis_prompt_before_stopping():
hunter, llm = _make_hunter(agent_mode="deep", max_steps=4, budget_usd=0.0)
Expand Down
Loading