Skip to content

Commit 85599f3

Browse files
Limit reflect structured output retries (#2433)
1 parent dd83bff commit 85599f3

3 files changed

Lines changed: 35 additions & 2 deletions

File tree

hindsight-api-slim/hindsight_api/engine/reflect/agent.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ def _json_schema_type_to_python(field_schema: dict) -> type:
239239
],
240240
response_format=DynamicModel,
241241
scope="reflect_structured",
242+
max_retries=1,
243+
initial_backoff=0.25,
244+
max_backoff=1.0,
242245
skip_validation=True, # We'll handle the dict ourselves
243246
return_usage=True,
244247
)

hindsight-api-slim/tests/test_reflect_agent.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
_clean_answer_text,
1919
_clean_done_answer,
2020
_count_messages_tokens,
21+
_generate_structured_output,
2122
_is_context_overflow_error,
2223
_is_done_tool,
2324
_normalize_tool_name,
@@ -234,6 +235,36 @@ def test_empty_list_is_vacuously_usable(self):
234235
assert _all_mental_models_are_usable_and_fresh({}) is True
235236

236237

238+
class TestReflectStructuredOutput:
239+
"""Tests for the second-pass structured-output extraction."""
240+
241+
@pytest.mark.asyncio
242+
async def test_structured_output_uses_short_retry_budget(self):
243+
"""A provider-specific structured-output failure must not consume the full reflect timeout."""
244+
llm = MagicMock()
245+
llm.call = AsyncMock(side_effect=RuntimeError("empty message content: finish_reason=length"))
246+
247+
result = await _generate_structured_output(
248+
answer="Alice prefers concise engineering updates.",
249+
response_schema={
250+
"type": "object",
251+
"properties": {
252+
"summary": {"type": "string"},
253+
},
254+
"required": ["summary"],
255+
},
256+
llm_config=llm,
257+
reflect_id="test-reflect",
258+
)
259+
260+
assert result.structured_output is None
261+
call_kwargs = llm.call.await_args.kwargs
262+
assert call_kwargs["scope"] == "reflect_structured"
263+
assert call_kwargs["max_retries"] == 1
264+
assert call_kwargs["initial_backoff"] == 0.25
265+
assert call_kwargs["max_backoff"] == 1.0
266+
267+
237268
class TestReflectAgentMocked:
238269
"""Test reflect agent with mocked LLM outputs."""
239270

@@ -1022,7 +1053,6 @@ async def test_reflect_completes_with_tiny_context_budget(self, memory, request_
10221053

10231054
# Patch get_config where memory_engine uses it, injecting a tiny
10241055
# max_context_tokens. Everything else delegates to the real config.
1025-
real_config = memory._get_raw_config() if hasattr(memory, "_get_raw_config") else None
10261056
from hindsight_api.config import get_config as _real_get_config
10271057

10281058
class _TinyContextProxy:

hindsight-api-slim/tests/test_retain_append_mode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ async def test_append_mode_concatenates_content(memory, request_context):
4343
assert "Alice works at Google" in v1_text
4444

4545
# Second retain with append — add new content
46-
v2_units = await memory.retain_batch_async(
46+
await memory.retain_batch_async(
4747
bank_id=bank_id,
4848
contents=[
4949
{

0 commit comments

Comments
 (0)