diff --git a/src/clean_agent_preview.py b/src/clean_agent_preview.py index 5d7d35f18..a3fba253d 100644 --- a/src/clean_agent_preview.py +++ b/src/clean_agent_preview.py @@ -3733,6 +3733,25 @@ def provider_request_messages(messages): return cleaned +def provider_wire_messages(messages): + """Drop invalid placeholders only at the provider serialization boundary.""" + cleaned = [] + for item in provider_request_messages(messages): + item.pop('_harness_control', None) + # A reasoning-only/contentless model turn may be followed by a + # harness-owned completion recovery. Persisting that empty assistant + # placeholder makes strict OpenAI-compatible providers reject the next + # request because neither content nor tool_calls is present. + if ( + item.get('role') == 'assistant' + and not item.get('content') + and not item.get('tool_calls') + ): + continue + cleaned.append(item) + return cleaned + + def record_tool_execution(executions, tool_event): """Persist one latest browser preview while live events can show every step.""" if canonical(tool_event.get('tool', '')) == 'private_browser' and tool_event.get('screenshot'): @@ -3765,10 +3784,10 @@ async def preview_model_response(client, endpoint_url, headers, request, recover request['messages'], max(1, int(message_context * recovery.get('scale', 1))), reserve_tokens=request['max_tokens'] + context_safety_margin(limit)) # Server-only provenance guides trimming, not the model's wire schema. - provider_request = {**request, 'messages': [ - {key: value for key, value in message.items() if key != '_harness_control'} - for message in request['messages'] - ]} + provider_request = { + **request, + 'messages': provider_wire_messages(request['messages']), + } response_started = False try: async with client.stream( diff --git a/tests/test_clean_agent_preview.py b/tests/test_clean_agent_preview.py index fc75b037e..3b134cd3e 100644 --- a/tests/test_clean_agent_preview.py +++ b/tests/test_clean_agent_preview.py @@ -85,6 +85,18 @@ def test_provider_request_messages_strips_internal_metadata_without_mutating_his assert history[0]['metadata']['trusted'] is False +def test_provider_wire_messages_drops_empty_assistant_placeholder(): + from src.clean_agent_preview import provider_wire_messages + + history = [ + {'role': 'assistant', 'content': None}, + {'role': 'user', 'content': 'Completion check: create the artifact.'}, + ] + + assert provider_request_messages(history) == history + assert provider_wire_messages(history) == [history[1]] + + def test_private_browser_observations_do_not_advance_page_revision(): assert private_browser_state_transition({'action': 'snapshot'}, 'https://example.org') == ( False, 'https://example.org')