From a12aead3e51cc3edb8a2a7599897304f5d5e5c6a Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Tue, 12 May 2026 00:07:53 +0200 Subject: [PATCH] fix(hypothesis): catch EmptyCompletionError dentro propose() invece di propagare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run phase2-5-qwen25-prompt-mut-001 fallito perché qwen-2.5-72b ha emesso empty completion ripetutamente; le 3 retry tenacity in LLMClient.complete si sono esaurite e l'exception è bollata fino a run_phase1, marcando l'intero run come failed (perso ~$0.017 di tier C). Fix: HypothesisAgent.propose() ora cattura EmptyCompletionError nel loop max_attempts trattandola come parse-fail "empty_completion" e ritentando con max_retries+1 tentativi (cumulativo: max_retries tenacity × max_attempts loop esterno, default 3×3 = 9 retry effettive). Se TUTTI i tentativi falliscono con empty, ritorna proposal con strategy=None e parse_error="empty_completion: ...", lasciando l'orchestrator continuare con quel genome marcato come "fitness=0" senza crash totale del run. +2 test: success dopo 1 empty + retry; failure totale dopo 3 empty consecutive. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/multi_swarm/agents/hypothesis.py | 11 +++++++-- tests/unit/test_hypothesis_agent.py | 36 +++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/multi_swarm/agents/hypothesis.py b/src/multi_swarm/agents/hypothesis.py index 00f5474..49ee4bc 100644 --- a/src/multi_swarm/agents/hypothesis.py +++ b/src/multi_swarm/agents/hypothesis.py @@ -4,7 +4,7 @@ import re from dataclasses import dataclass, field from ..genome.hypothesis import HypothesisAgentGenome -from ..llm.client import CompletionResult, LLMClient +from ..llm.client import CompletionResult, EmptyCompletionError, LLMClient from ..protocol.parser import ParseError, Strategy, parse_strategy from ..protocol.validator import ValidationError, validate_strategy @@ -274,7 +274,14 @@ class HypothesisAgent: previous_error=errors[-1], ) - completion = self._llm.complete(genome, system=system, user=user) + try: + completion = self._llm.complete(genome, system=system, user=user) + except EmptyCompletionError as e: + # LLM esaurito retry tenacity senza una risposta. Tratta come + # parse-fail "empty" e ritenta nel loop esterno (max_attempts). + errors.append(f"empty_completion: {e}") + last_raw = "" + continue completions.append(completion) last_raw = completion.text diff --git a/tests/unit/test_hypothesis_agent.py b/tests/unit/test_hypothesis_agent.py index 7050473..23a3136 100644 --- a/tests/unit/test_hypothesis_agent.py +++ b/tests/unit/test_hypothesis_agent.py @@ -2,7 +2,7 @@ import json from multi_swarm.agents.hypothesis import HypothesisAgent, MarketSummary from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier -from multi_swarm.llm.client import CompletionResult +from multi_swarm.llm.client import CompletionResult, EmptyCompletionError def make_summary() -> MarketSummary: @@ -214,3 +214,37 @@ def test_hypothesis_agent_no_retry_when_first_succeeds(mocker): # type: ignore[ assert proposal.n_attempts == 1 assert len(proposal.completions) == 1 assert fake_llm.complete.call_count == 1 + + +def test_hypothesis_agent_retries_on_empty_completion(mocker): # type: ignore[no-untyped-def] + """LLMClient esaurisce retry tenacity → propose ritenta nel loop max_attempts.""" + fake_llm = mocker.MagicMock() + fake_llm.complete.side_effect = [ + EmptyCompletionError("empty response from qwen"), + CompletionResult( + text=VALID_STRATEGY_JSON, + input_tokens=200, + output_tokens=80, + tier=ModelTier.C, + model="qwen", + ), + ] + agent = HypothesisAgent(llm=fake_llm, max_retries=2) + proposal = agent.propose(make_genome(), make_summary()) + assert proposal.strategy is not None + assert fake_llm.complete.call_count == 2 + # n_attempts conta solo le completions arrivate (skipping empty failures). + assert len(proposal.completions) == 1 + + +def test_hypothesis_agent_returns_failed_proposal_on_only_empty_completions(mocker): # type: ignore[no-untyped-def] + """Tutti i tentativi sollevano EmptyCompletionError → proposal con strategy None.""" + fake_llm = mocker.MagicMock() + fake_llm.complete.side_effect = EmptyCompletionError("empty response") + agent = HypothesisAgent(llm=fake_llm, max_retries=2) + proposal = agent.propose(make_genome(), make_summary()) + assert proposal.strategy is None + assert proposal.parse_error is not None + assert "empty_completion" in proposal.parse_error + # 3 tentativi tutti falliti. + assert fake_llm.complete.call_count == 3