feat(llm): add session_id for retries and summarize for run tracking

HypothesisAgent now opens an OpusAgent session on first attempt and
reuses it for parse-error retries — Claude sees full conversation
context instead of re-stuffing the prompt. Sessions are closed after
propose() completes. All completions set summarize=true so topics
accumulate incremental summaries of GA activity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-05-26 09:18:11 +00:00
parent 646c64dacd
commit ba2c23e602
3 changed files with 123 additions and 50 deletions
@@ -288,19 +288,12 @@ def _render_focus_block(keys: list[str], market: MarketSummary) -> str:
_RETRY_TEMPLATE = """\
{original_user}
Il JSON che hai generato contiene un errore: {previous_error}
--- TENTATIVO PRECEDENTE FALLITO ---
Output: {previous_raw}
Errore: {previous_error}
---
Correggi l'errore e rispondi di nuovo con un singolo oggetto JSON valido
dentro fence ```json...```, seguendo strettamente lo schema fornito nel
SYSTEM message.
Correggi e rispondi di nuovo con un singolo oggetto JSON valido
dentro fence ```json...```, seguendo strettamente lo schema fornito.
"""
_RETRY_RAW_TRUNCATE = 800
_JSON_FENCE_RE = re.compile(
r"```(?:json)?\s*(\{[\s\S]*\})\s*```",
@@ -429,20 +422,23 @@ class HypothesisAgent:
errors: list[str] = []
last_raw = ""
max_attempts = 1 + self._max_retries
session_id: str | None = None
try:
for attempt in range(max_attempts):
if attempt == 0:
user = original_user
req_session_id = "new"
else:
truncated = last_raw[:_RETRY_RAW_TRUNCATE]
user = _RETRY_TEMPLATE.format(
original_user=original_user,
previous_raw=truncated,
previous_error=errors[-1],
)
user = _RETRY_TEMPLATE.format(previous_error=errors[-1])
req_session_id = session_id or "new"
try:
completion = self._llm.complete(genome, system=system, user=user)
completion = self._llm.complete(
genome, system=system, user=user,
session_id=req_session_id,
summarize=True,
)
except EmptyCompletionError as e:
errors.append(f"empty_completion: {e}")
last_raw = ""
@@ -457,6 +453,8 @@ class HypothesisAgent:
continue
completions.append(completion)
last_raw = completion.text
if completion.session_id:
session_id = completion.session_id
strategy, err = _try_parse(completion.text)
if strategy is not None:
@@ -469,6 +467,9 @@ class HypothesisAgent:
)
assert err is not None
errors.append(err)
finally:
if session_id:
self._llm.close_session(session_id)
chained = " | ".join(
f"attempt {i + 1}: {e}" for i, e in enumerate(errors)
@@ -54,6 +54,7 @@ class CompletionResult:
output_tokens: int
tier: ModelTier
model: str
session_id: str | None = None
class LLMClient:
@@ -138,6 +139,12 @@ class LLMClient:
f"Request {request_id} timed out after {self._poll_timeout}s"
)
def close_session(self, session_id: str) -> None:
try:
self._client.delete(f"/api/sessions/{session_id}")
except httpx.HTTPError:
logger.debug("Failed to close session %s", session_id)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=2.0, min=2.0, max=30.0),
@@ -150,15 +157,23 @@ class LLMClient:
system: str,
user: str,
max_tokens: int = 2000,
session_id: str | None = None,
summarize: bool = False,
) -> CompletionResult:
model = self._tier_map[genome.model_tier]
topic_id = self._get_or_create_topic(system)
resp = self._client.post("/api/requests", json={
body: dict = {
"topic_id": topic_id,
"prompt": user,
"model": model,
})
}
if session_id is not None:
body["session_id"] = session_id
if summarize:
body["summarize"] = True
resp = self._client.post("/api/requests", json=body)
if resp.status_code == 429:
raise OpusAgentTransientError("Rate limited")
@@ -180,4 +195,5 @@ class LLMClient:
output_tokens=0,
tier=genome.model_tier,
model=model,
session_id=result.get("session_id"),
)
@@ -34,13 +34,16 @@ REQUEST_ACCEPTED = {
}
def _completed_response(text: str = "(strategy ...)") -> dict:
def _completed_response(
text: str = "(strategy ...)", session_id: str = "sess-789",
) -> dict:
return {
"success": True,
"data": {
"id": "req-456",
"status": "completed",
"result": text,
"session_id": session_id,
},
}
@@ -212,3 +215,56 @@ def test_tokens_are_zero():
assert out.input_tokens == 0
assert out.output_tokens == 0
def test_session_id_returned_from_completion():
transport = _mock_transport([
httpx.Response(201, json=TOPIC_RESPONSE),
httpx.Response(202, json=REQUEST_ACCEPTED),
httpx.Response(200, json=_completed_response(session_id="sess-abc")),
])
client = _make_client(transport)
out = client.complete(make_genome(ModelTier.C), system="sys", user="usr", session_id="new")
assert out.session_id == "sess-abc"
def test_session_id_and_summarize_sent_in_request():
requests_seen: list[dict] = []
def handler(request: httpx.Request) -> httpx.Response:
if request.method == "POST" and "/requests" in str(request.url):
import json
requests_seen.append(json.loads(request.content))
return httpx.Response(202, json=REQUEST_ACCEPTED)
if request.method == "POST" and "/topics" in str(request.url):
return httpx.Response(201, json=TOPIC_RESPONSE)
return httpx.Response(200, json=_completed_response())
transport = httpx.MockTransport(handler)
client = _make_client(transport)
client.complete(
make_genome(ModelTier.C), system="sys", user="usr",
session_id="sess-existing", summarize=True,
)
assert len(requests_seen) == 1
assert requests_seen[0]["session_id"] == "sess-existing"
assert requests_seen[0]["summarize"] is True
def test_close_session():
deleted: list[str] = []
def handler(request: httpx.Request) -> httpx.Response:
if request.method == "DELETE":
deleted.append(str(request.url))
return httpx.Response(200, json={"success": True})
return httpx.Response(200, json={"success": True})
transport = httpx.MockTransport(handler)
client = _make_client(transport)
client.close_session("sess-to-close")
assert len(deleted) == 1
assert "sess-to-close" in deleted[0]