@@ -204,6 +204,69 @@ async def test_empty_response_retry_budget_resets_after_successful_turn( |
| 204 | 204 | assert sum("retry 1/2" in message for message in retry_messages) >= 2 |
| 205 | 205 | |
| 206 | 206 | |
| 207 | +@pytest.mark.asyncio |
| 208 | +async def test_empty_response_retry_replaces_prior_user_interruption_handoff( |
| 209 | + temp_dir: Path, |
| 210 | +) -> None: |
| 211 | + first = temp_dir / "index.html" |
| 212 | + second = temp_dir / "chapters" / "01-introduction.html" |
| 213 | + backend = ScriptedBackend( |
| 214 | + completions=[ |
| 215 | + CompletionResponse( |
| 216 | + content="I'll create the guide index now.", |
| 217 | + tool_calls=[ |
| 218 | + ToolCall( |
| 219 | + id="write-1", |
| 220 | + name="write", |
| 221 | + arguments={ |
| 222 | + "file_path": str(first), |
| 223 | + "content": "<html><a href=\"chapters/01-introduction.html\">Intro</a></html>\n", |
| 224 | + }, |
| 225 | + ) |
| 226 | + ], |
| 227 | + ), |
| 228 | + CompletionResponse(content=""), |
| 229 | + CompletionResponse( |
| 230 | + content="I'll create the chapter now.", |
| 231 | + tool_calls=[ |
| 232 | + ToolCall( |
| 233 | + id="write-2", |
| 234 | + name="write", |
| 235 | + arguments={ |
| 236 | + "file_path": str(second), |
| 237 | + "content": "<html></html>\n", |
| 238 | + }, |
| 239 | + ) |
| 240 | + ], |
| 241 | + ), |
| 242 | + CompletionResponse(content="Done."), |
| 243 | + ] |
| 244 | + ) |
| 245 | + |
| 246 | + run = await run_scenario( |
| 247 | + "Create index.html and a first chapter file.", |
| 248 | + backend, |
| 249 | + config=non_streaming_config(), |
| 250 | + project_root=temp_dir, |
| 251 | + ) |
| 252 | + |
| 253 | + assert run.response.startswith("Done.") |
| 254 | + retry_invocation_messages = backend.invocations[2].messages |
| 255 | + user_steering_messages = [ |
| 256 | + message.content |
| 257 | + for message in retry_invocation_messages |
| 258 | + if message.role == Role.USER |
| 259 | + and ( |
| 260 | + "[EMPTY ASSISTANT RESPONSE]" in message.content |
| 261 | + or "[USER INTERRUPTION]:" in message.content |
| 262 | + or "[CONTINUE CURRENT STEP]" in message.content |
| 263 | + ) |
| 264 | + ] |
| 265 | + assert len(user_steering_messages) == 1 |
| 266 | + assert user_steering_messages[0].startswith("[EMPTY ASSISTANT RESPONSE]") |
| 267 | + assert "[USER INTERRUPTION]:" not in user_steering_messages[0] |
| 268 | + |
| 269 | + |
| 207 | 270 | @pytest.mark.asyncio |
| 208 | 271 | async def test_empty_response_retry_budget_resets_after_todowrite_turn( |
| 209 | 272 | temp_dir: Path, |