|
18 | 18 | _clean_answer_text, |
19 | 19 | _clean_done_answer, |
20 | 20 | _count_messages_tokens, |
| 21 | + _generate_structured_output, |
21 | 22 | _is_context_overflow_error, |
22 | 23 | _is_done_tool, |
23 | 24 | _normalize_tool_name, |
@@ -234,6 +235,36 @@ def test_empty_list_is_vacuously_usable(self): |
234 | 235 | assert _all_mental_models_are_usable_and_fresh({}) is True |
235 | 236 |
|
236 | 237 |
|
| 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 | + |
237 | 268 | class TestReflectAgentMocked: |
238 | 269 | """Test reflect agent with mocked LLM outputs.""" |
239 | 270 |
|
@@ -1022,7 +1053,6 @@ async def test_reflect_completes_with_tiny_context_budget(self, memory, request_ |
1022 | 1053 |
|
1023 | 1054 | # Patch get_config where memory_engine uses it, injecting a tiny |
1024 | 1055 | # 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 |
1026 | 1056 | from hindsight_api.config import get_config as _real_get_config |
1027 | 1057 |
|
1028 | 1058 | class _TinyContextProxy: |
|
0 commit comments