diff --git a/tests/agent/test_responses_empty_tool_replay.py b/tests/agent/test_responses_empty_tool_replay.py index 452f419221..caad38b906 100644 --- a/tests/agent/test_responses_empty_tool_replay.py +++ b/tests/agent/test_responses_empty_tool_replay.py @@ -4,6 +4,9 @@ Regression investigation for #103483: synthetic Muse tool-loop A/B reproduction. Also covers #75202: the lone-reasoning follower must be non-empty for strict Responses-compatible providers that reject "" with 400. """ +import json +from pathlib import Path + import pytest from agent.codex_responses_adapter import _chat_messages_to_responses_input @@ -29,3 +32,32 @@ def test_reasoning_without_tool_keeps_nonempty_following_item(): 'encrypted_content': 'synthetic-encrypted-fixture', 'summary': []}], }]) assert items[-1] == {'role': 'assistant', 'content': ' '} + + +def test_failing_turn_fixture_emits_no_invented_carrier(): + """Real failing-turn shape from #103483 (fixture by @cristianbdev, see + https://gist.github.com/cristianbdev/6036f7aa3838935adfaeb9b6800f7450 — + 3 reasoning+tool assistant rows, 5 tool calls/outputs, content redacted). + + The adapter must invent no blank carrier, must keep every reasoning item + directly followed by a function_call, and must preserve all call/output + pairings. + """ + fixture_path = ( + Path(__file__).resolve().parent.parent + / "fixtures" / "spark_failing_turn_shape.json" + ) + messages = json.loads(fixture_path.read_text(encoding="utf-8")) + items = _chat_messages_to_responses_input(messages) + assert {"role": "assistant", "content": ""} not in items + for i, item in enumerate(items): + if item.get("type") == "reasoning": + # Reasoning items may cluster; the next non-reasoning item after + # each of them must be the function_call that follows it — never + # an invented blank assistant message. + nxt = next(it for it in items[i + 1:] if it.get("type") != "reasoning") + assert nxt.get("type") == "function_call" + calls = [item["call_id"] for item in items if item.get("type") == "function_call"] + outputs = [item["call_id"] for item in items if item.get("type") == "function_call_output"] + assert len(calls) == 5 + assert sorted(calls) == sorted(outputs) diff --git a/tests/fixtures/spark_failing_turn_shape.json b/tests/fixtures/spark_failing_turn_shape.json new file mode 100644 index 0000000000..c6f3c3d45e --- /dev/null +++ b/tests/fixtures/spark_failing_turn_shape.json @@ -0,0 +1,91 @@ +[ + { + "role": "user", + "content": "Investigate the installed 'agy' CLI and summarize its version and capabilities in one report." + }, + { + "role": "assistant", + "content": "", + "codex_reasoning_items": [ + {"type": "reasoning", "id": "rs_fixture_01", "summary": [{"type": "summary_text", "text": "Batched three tool calls to inspect the CLI."}], "encrypted_content": "synthetic-encrypted-content-placeholder-0001-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}, + {"type": "reasoning", "id": "rs_fixture_02", "summary": [], "encrypted_content": "synthetic-encrypted-content-placeholder-0002-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"} + ], + "tool_calls": [ + { + "id": "call_fixture_01", + "call_id": "call_fixture_01", + "type": "function", + "function": {"name": "terminal", "arguments": "{\"command\": \"which agy; agy --version\"}"} + } + ] + }, + { + "role": "tool", + "tool_call_id": "call_fixture_01", + "content": "[tool output elided — 11,269 chars in the original failing run]" + }, + { + "role": "assistant", + "content": "", + "codex_reasoning_items": [ + {"type": "reasoning", "id": "rs_fixture_03", "summary": [], "encrypted_content": "synthetic-encrypted-content-placeholder-0003-CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"}, + {"type": "reasoning", "id": "rs_fixture_04", "summary": [], "encrypted_content": "synthetic-encrypted-content-placeholder-0004-DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"} + ], + "tool_calls": [ + { + "id": "call_fixture_02", + "call_id": "call_fixture_02", + "type": "function", + "function": {"name": "terminal", "arguments": "{\"command\": \"agy agent --help\"}"} + } + ] + }, + { + "role": "tool", + "tool_call_id": "call_fixture_02", + "content": "[tool output elided — 3,169 chars in the original failing run]" + }, + { + "role": "assistant", + "content": "", + "codex_reasoning_items": [ + {"type": "reasoning", "id": "rs_fixture_05", "summary": [], "encrypted_content": "synthetic-encrypted-content-placeholder-0005-EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"} + ], + "tool_calls": [ + { + "id": "call_fixture_03", + "call_id": "call_fixture_03", + "type": "function", + "function": {"name": "terminal", "arguments": "{\"command\": \"agy mcp --help\"}"} + }, + { + "id": "call_fixture_04", + "call_id": "call_fixture_04", + "type": "function", + "function": {"name": "terminal", "arguments": "{\"command\": \"agy changelog | head -40\"}"} + }, + { + "id": "call_fixture_05", + "call_id": "call_fixture_05", + "type": "function", + "function": {"name": "terminal", "arguments": "{\"command\": \"ls ~/.config/agy/ 2>&1 || true\"}"} + } + ] + }, + { + "role": "tool", + "tool_call_id": "call_fixture_03", + "content": "[tool output elided — 1,494 chars in the original failing run]" + }, + { + "role": "tool", + "tool_call_id": "call_fixture_04", + "content": "[tool output elided — 32,686 chars in the original failing run]" + }, + { + "role": "tool", + "tool_call_id": "call_fixture_05", + "content": "[tool output elided — 598 chars in the original failing run]" + } +] +