From 68238f0b6a2e609a1d42b36425bf1558728f333c Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 30 Jul 2026 11:21:18 +0300 Subject: [PATCH] Name the readlines workaround when sandbox code iterates a file 50 executions across 40 cases in an 822-case run died on '_io.TextIOWrapper' object is not iterable, and those cases scored 37.5% judged against 60.1% and cited 12.5% against 55.2%. --- CHANGELOG.md | 1 + .../haiku/rag/capabilities/analysis.py | 20 ++++++++++++- tests/capabilities/test_capabilities.py | 29 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c775455..838472a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixed +- A failed `analysis_execute_code` call that iterated a file object reports the `.readlines()` workaround alongside the `TypeError`. - A capability that reaches its request limit keeps its cite tool for two further model requests while its other tools are removed, so an exhausted run can still register citations. - A cited chunk id that does not match a retrieved id exactly resolves to the closest one above a 0.75 similarity cutoff, recovering ids mistyped from search results. - Dotfiles are parsed as their actual format instead of a single unstructured text block. Docling ignores the extension of a name starting with a dot, so converters strip leading dots from the name they hand it. diff --git a/haiku_rag_slim/haiku/rag/capabilities/analysis.py b/haiku_rag_slim/haiku/rag/capabilities/analysis.py index 2d754840..cea8a878 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/analysis.py +++ b/haiku_rag_slim/haiku/rag/capabilities/analysis.py @@ -37,6 +37,21 @@ def instructions() -> str: return _instructions_path.read_text().strip() +def _recovery_hint(stderr: str) -> str: + """Name the workaround for sandbox limits models trip over repeatedly. + + The instructions already say file objects are not iterable, and models write + ``for line in open(...)`` regardless. Carrying the fix in the error gives + them something to act on for the retry. + """ + if "TextIOWrapper" in stderr and "not iterable" in stderr: + return ( + "\n\nHint: file objects cannot be iterated here. Read lines with " + '.readlines() or .read().split("\\n").' + ) + return "" + + @dataclass class AnalysisCapability(RAGCapabilityBase[AnalysisState]): """Deferred capability for sandboxed computation over a RAG corpus.""" @@ -103,7 +118,10 @@ class AnalysisCapability(RAGCapabilityBase[AnalysisState]): ) ) if not result.success: - raise ToolFailed(f"{result.stderr}\n\nOutput: {result.stdout}") + raise ToolFailed( + f"{result.stderr}{_recovery_hint(result.stderr)}" + f"\n\nOutput: {result.stdout}" + ) return result.stdout or "No output." def get_toolset(self) -> FunctionToolset[Any]: diff --git a/tests/capabilities/test_capabilities.py b/tests/capabilities/test_capabilities.py index 45dc36cf..ac99b1d9 100644 --- a/tests/capabilities/test_capabilities.py +++ b/tests/capabilities/test_capabilities.py @@ -589,6 +589,35 @@ async def test_cite_tool_is_withdrawn_after_the_grace_window(temp_db_path): assert kept == [] +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("stderr", "expect_hint"), + [ + ("TypeError: '_io.TextIOWrapper' object is not iterable", True), + ("TypeError: 'list' object is not an iterator", False), + ], +) +async def test_sandbox_iteration_failure_carries_the_workaround( + temp_db_path, stderr, expect_hint +): + """A model that iterates a file object gets told what to do instead.""" + capability = create_analysis(db_path=temp_db_path, config=AppConfig()) + capability.state = AnalysisState() + sandbox = AsyncMock() + sandbox.execute.return_value = SandboxResult( + stdout="", stderr=stderr, success=False + ) + sandbox._search_results = [] + capability.sandbox = cast(Sandbox, sandbox) + + with pytest.raises(ToolFailed) as failure: + await capability._execute_code( + "for line in open('/documents/x/items.jsonl'): pass" + ) + + assert (".readlines()" in str(failure.value)) is expect_hint + + @pytest.mark.asyncio async def test_analysis_sandbox_failure_records_execution_and_fails_the_tool( temp_db_path,