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%.
This commit is contained in:
Yiorgis Gozadinos 2026-07-30 11:21:18 +03:00
parent 485a8f901e
commit 68238f0b6a
No known key found for this signature in database
3 changed files with 49 additions and 1 deletions

View file

@ -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.

View file

@ -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]:

View file

@ -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,