tenseleyflow/loader / 332bab7

Browse files

Replace stacked retry nudges

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
332bab755c972e5f09caa1a2df650f5bfaed60c4
Parents
89dbb36
Tree
513368b

2 changed files

StatusFile+-
M src/loader/runtime/turn_iteration.py 13 2
M tests/test_runtime_repair_flows.py 63 0
src/loader/runtime/turn_iteration.pymodified
@@ -270,8 +270,8 @@ class TurnIterationController:
270270
             if (
271271
                 self.context.session.messages
272272
                 and self.context.session.messages[-1].role == Role.USER
273
-                and self.context.session.messages[-1].content.startswith(
274
-                    "[EMPTY ASSISTANT RESPONSE]"
273
+                and self._should_replace_last_steering_message(
274
+                    self.context.session.messages[-1].content
275275
                 )
276276
             ):
277277
                 self.context.session.messages[-1] = retry_message
@@ -311,3 +311,14 @@ class TurnIterationController:
311311
             finalize_reason_code=empty_decision.reason_code,
312312
             finalize_reason_summary=empty_decision.reason_summary,
313313
         )
314
+
315
+    @staticmethod
316
+    def _should_replace_last_steering_message(content: str) -> bool:
317
+        return content.startswith(
318
+            (
319
+                "[EMPTY ASSISTANT RESPONSE]",
320
+                "[USER INTERRUPTION]:",
321
+                "[CONTINUE CURRENT STEP]",
322
+                "[PLANNED ARTIFACTS STILL MISSING]",
323
+            )
324
+        )
tests/test_runtime_repair_flows.pymodified
@@ -204,6 +204,69 @@ async def test_empty_response_retry_budget_resets_after_successful_turn(
204204
     assert sum("retry 1/2" in message for message in retry_messages) >= 2
205205
 
206206
 
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
+
207270
 @pytest.mark.asyncio
208271
 async def test_empty_response_retry_budget_resets_after_todowrite_turn(
209272
     temp_dir: Path,