fix(hypothesis): catch EmptyCompletionError dentro propose() invece di propagare

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 00:07:53 +02:00
parent ec80af9f26
commit a12aead3e5
2 changed files with 44 additions and 3 deletions
+8 -1
View File
@@ -4,7 +4,7 @@ import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from ..genome.hypothesis import HypothesisAgentGenome 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.parser import ParseError, Strategy, parse_strategy
from ..protocol.validator import ValidationError, validate_strategy from ..protocol.validator import ValidationError, validate_strategy
@@ -274,7 +274,14 @@ class HypothesisAgent:
previous_error=errors[-1], previous_error=errors[-1],
) )
try:
completion = self._llm.complete(genome, system=system, user=user) 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) completions.append(completion)
last_raw = completion.text last_raw = completion.text
+35 -1
View File
@@ -2,7 +2,7 @@ import json
from multi_swarm.agents.hypothesis import HypothesisAgent, MarketSummary from multi_swarm.agents.hypothesis import HypothesisAgent, MarketSummary
from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier 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: 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 proposal.n_attempts == 1
assert len(proposal.completions) == 1 assert len(proposal.completions) == 1
assert fake_llm.complete.call_count == 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