From b4cd39657d0c6a603c0163ef6c7a80718be55d96 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Sat, 19 Sep 2026 02:07:04 +0000 Subject: [PATCH] recover mixed artifact tool payloads --- HARNESS_VERSION | 2 +- src/clean_agent_preview.py | 50 +++++++++++++++++++++++++++++++ tests/test_clean_agent_preview.py | 48 ++++++++++++++++++++++++----- tests/test_harness_version.py | 2 +- 4 files changed, 92 insertions(+), 10 deletions(-) diff --git a/HARNESS_VERSION b/HARNESS_VERSION index 3f9edc439..a64d245a9 100644 --- a/HARNESS_VERSION +++ b/HARNESS_VERSION @@ -1 +1 @@ -0.20.12 +0.20.13 diff --git a/src/clean_agent_preview.py b/src/clean_agent_preview.py index 2dde3e4d9..36a9a2a29 100644 --- a/src/clean_agent_preview.py +++ b/src/clean_agent_preview.py @@ -1603,6 +1603,11 @@ def required_artifact_completion_tool_choice(required_artifacts, offered_schemas targets = [str(path or '').strip().rstrip('/') for path in required_artifacts] if not targets: return None + # A required directory commonly contains several files and may need a + # programmatic extractor. Requiring *a* tool call preserves forward + # progress without trapping the model in one enormous write_file payload. + if len(targets) == 1 and not Path(targets[0]).suffix: + return 'required' offered = { canonical((schema.get('function') or {}).get('name')): (schema.get('function') or {}).get('name') @@ -1688,6 +1693,51 @@ def expand_concatenated_write_calls(calls, *, max_calls=16): if len(items) > max_calls: raise ValueError('too many concatenated write calls') except (TypeError, ValueError, json.JSONDecodeError): + # Qwen's native parser can leave one complete, grounded writer + # object followed by textual blocks in the same + # argument. Execute only that leading writer. The trailing calls + # may depend on reads that have not run yet, so they stay inert and + # the next model round can propose them normally with fresh + # evidence. + try: + leading, end = decoder.raw_decode(raw) + except (TypeError, ValueError, json.JSONDecodeError): + leading, end = None, 0 + tail = raw[end:].strip() if end else '' + markup_openers = tail.count('') + markup_closers = tail.count('') + bounded_markup = ( + tail.startswith('') + and 1 <= markup_openers <= 32 + # A provider may truncate the last textual call at its output + # limit. It remains inert; only the complete leading JSON + # writer is recovered. + and markup_closers in {markup_openers, markup_openers - 1} + and not strip_tool_blocks( + tail, + skip_fenced=True, + additional_tool_names=('bash', 'python', 'read_file', 'write_file'), + ).strip() + ) + if ( + bounded_markup + and isinstance(leading, dict) + and set(leading) == {'path', 'content'} + and isinstance(leading['path'], str) + and bool(leading['path'].strip()) + and isinstance(leading['content'], str) + ): + base_id = str(call.get('id') or 'call_write') + expanded.append({ + 'id': f'{base_id}_0', + 'type': 'function', + 'function': { + 'name': function.get('name', 'write_file'), + 'arguments': json.dumps(leading, ensure_ascii=False), + }, + }) + recovered_count += 1 + continue expanded.append(call) continue if not (2 <= len(items) <= max_calls) or not all( diff --git a/tests/test_clean_agent_preview.py b/tests/test_clean_agent_preview.py index d199dc8ac..4b5891a87 100644 --- a/tests/test_clean_agent_preview.py +++ b/tests/test_clean_agent_preview.py @@ -684,7 +684,7 @@ def test_required_binary_artifact_forces_python_instead_of_text_writer(): ] -def test_required_directory_artifact_forces_writer_with_descendant_schema(): +def test_required_directory_artifact_allows_any_offered_mutation_tool(): offered = [ {'type': 'function', 'function': {'name': 'bash'}}, {'type': 'function', 'function': {'name': 'write_file'}}, @@ -692,7 +692,7 @@ def test_required_directory_artifact_forces_writer_with_descendant_schema(): assert required_artifact_completion_tool_choice( ('/tmp_workspace/results',), offered, - ) == {'type': 'function', 'function': {'name': 'write_file'}} + ) == 'required' def test_action_promise_response_rejects_future_work_but_not_real_answers(): @@ -1035,6 +1035,38 @@ def test_concatenated_writer_recovery_fails_closed_for_ambiguous_or_large_batche assert expand_concatenated_write_calls([too_large]) == ([too_large], 0) +def test_mixed_writer_argument_recovers_only_grounded_leading_json_write(): + from src.clean_agent_preview import expand_concatenated_write_calls + + first = {'path': '/workspace/results/1.tex', 'content': 'grounded table'} + trailing = ''' + + +sed -n '2,3p' /workspace/paper.tex + + + + +/workspace/results/2.tex +ungrounded table + + +''' + source = { + 'id': 'mixed', 'type': 'function', 'function': { + 'name': 'write_file', + 'arguments': json.dumps(first) + trailing, + }, + } + + recovered, count = expand_concatenated_write_calls([source]) + + assert count == 1 + assert len(recovered) == 1 + assert recovered[0]['id'] == 'mixed_0' + assert json.loads(recovered[0]['function']['arguments']) == first + + def test_malformed_writer_handoff_uses_descendant_file_not_required_directory(): from src.clean_agent_preview import malformed_write_handoff_target @@ -5527,7 +5559,7 @@ async def test_native_stream_reserves_remaining_budget_for_required_artifact(mon ] contract = resolve_full_inventory_contract(schemas=schemas, policy=ToolPolicy()) raw = [chunk async for chunk in stream_preview( - endpoint_url="http://test", model="deepseek-flash", + endpoint_url="http://test", model="test", messages=[{"role": "user", "content": "Research and create the requested output."}], headers={}, turn_contract=contract, session_id="test", owner="test", disabled_tools=set(), tool_policy=ToolPolicy(), workspace="/tmp/workspace", @@ -5562,16 +5594,16 @@ async def test_native_stream_reserves_remaining_budget_for_required_artifact(mon ) assert "write_file" in request_contract["offered_tools"] assert "web_search" not in request_contract["offered_tools"] - assert request_contract["tool_choice"] is None + assert request_contract["tool_choice"] == "required" reserved_names = [tool["function"]["name"] for tool in requests[12]["tools"]] - assert reserved_names == ["write_file"] - assert "tool_choice" not in requests[12] - directory_path = requests[12]["tools"][0]["function"]["parameters"]["properties"]["path"] + assert reserved_names == ["bash", "python", "write_file"] + assert requests[12]["tool_choice"] == "required" + directory_path = requests[12]["tools"][2]["function"]["parameters"]["properties"]["path"] assert re.search(directory_path["pattern"], "/tmp_workspace/results/out.md") assert not re.search(directory_path["pattern"], "/tmp_workspace/results") assert "create one or more files inside" in requests[12]["messages"][-1]["content"] assert "Do not pass the directory itself as a file path" in requests[12]["messages"][-1]["content"] - assert "bash" not in executed + assert "bash" in executed assert any( event.get("type") == "tool_output" and event.get("tool") == "write_file" and not event.get("error") for event in events diff --git a/tests/test_harness_version.py b/tests/test_harness_version.py index b043333dd..f10cc475a 100644 --- a/tests/test_harness_version.py +++ b/tests/test_harness_version.py @@ -8,7 +8,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_original_harness_version_is_canonical_semver() -> None: version = (ROOT / "HARNESS_VERSION").read_text(encoding="utf-8").strip() - assert version == "0.20.12" + assert version == "0.20.13" assert re.fullmatch(r"\d+\.\d+\.\d+", version)