diff --git a/HARNESS_VERSION b/HARNESS_VERSION index 752e63038..e7c6f5dbf 100644 --- a/HARNESS_VERSION +++ b/HARNESS_VERSION @@ -1 +1 @@ -0.20.6 +0.20.7 diff --git a/src/clean_agent_preview.py b/src/clean_agent_preview.py index 3f86c6656..356e26a16 100644 --- a/src/clean_agent_preview.py +++ b/src/clean_agent_preview.py @@ -1571,8 +1571,15 @@ def artifact_completion_tool_schemas(offered_schemas, required_artifacts): def required_artifact_completion_tool_choice(required_artifacts, offered_schemas): - """Choose Python for one binary output; preserve the text-writer default.""" + """Choose a writer only when the contract names an exact output file. + + A directory contract needs the model to choose one or more child filenames. + Forcing ``write_file`` there encourages an impossible write to the directory + path itself, so leave tool choice unconstrained for that case. + """ targets = [str(path or '').strip().rstrip('/') for path in required_artifacts] + if not targets or any(not Path(target).suffix for target in targets): + return None offered = { canonical((schema.get('function') or {}).get('name')): (schema.get('function') or {}).get('name') @@ -4451,6 +4458,17 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac and calls >= min(NATIVE_ARTIFACT_RESEARCH_LIMIT, tool_call_limit - 1) ): artifact_write_phase = True + directory_artifact_guidance = '' + directory_targets = [ + path for path in required_artifacts + if not Path(str(path or '').strip().rstrip('/')).suffix + ] + if directory_targets: + directory_artifact_guidance = ( + ' Each listed directory is a container: create one or more files ' + 'inside it with meaningful content. Do not pass the directory itself ' + 'as a file path.' + ) history.append({ 'role': 'user', '_harness_control': True, @@ -4459,7 +4477,9 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac 'unwritten after substantial research: ' + ', '.join(required_artifacts) + '. Use the evidence already gathered and the offered workspace tools ' - 'to create and verify the required outputs now. Do not continue broad ' + 'to create and verify the required outputs now.' + + directory_artifact_guidance + + ' Do not continue broad ' 'web, document, or media research.' ), }) diff --git a/tests/test_clean_agent_preview.py b/tests/test_clean_agent_preview.py index c2d941d7c..58e1dd032 100644 --- a/tests/test_clean_agent_preview.py +++ b/tests/test_clean_agent_preview.py @@ -679,6 +679,17 @@ def test_required_binary_artifact_forces_python_instead_of_text_writer(): ] +def test_required_directory_artifact_does_not_force_writer_to_directory_path(): + offered = [ + {'type': 'function', 'function': {'name': 'bash'}}, + {'type': 'function', 'function': {'name': 'write_file'}}, + ] + + assert required_artifact_completion_tool_choice( + ('/tmp_workspace/results',), offered, + ) is None + + def test_action_promise_response_rejects_future_work_but_not_real_answers(): import src.clean_agent_preview as module @@ -5317,9 +5328,11 @@ async def test_native_stream_reserves_remaining_budget_for_required_artifact(mon assert "web_search" not in request_contract["offered_tools"] assert request_contract["tool_choice"] is None reserved_names = [tool["function"]["name"] for tool in requests[12]["tools"]] - assert reserved_names == ["write_file"] + assert reserved_names == ["bash", "python", "write_file"] assert "tool_choice" not in requests[12] - assert "bash" not in executed + 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" 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 4bf59e21f..6f70ed371 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.6" + assert version == "0.20.7" assert re.fullmatch(r"\d+\.\d+\.\d+", version)