@@ -275,6 +275,22 @@ class ResponseRepairer: |
| 275 | 275 | retry_number: int, |
| 276 | 276 | max_empty_retries: int, |
| 277 | 277 | ) -> str: |
| 278 | + blocked_asset_lines = self._blocked_html_asset_empty_retry_lines(dod) |
| 279 | + if blocked_asset_lines: |
| 280 | + return "\n".join( |
| 281 | + [ |
| 282 | + "[EMPTY ASSISTANT RESPONSE]", |
| 283 | + ( |
| 284 | + "Your last response was empty " |
| 285 | + f"(retry {retry_number}/{max_empty_retries}) after a blocked " |
| 286 | + "HTML asset reference. Retry the blocked mutation directly." |
| 287 | + ), |
| 288 | + *[f"- {line}" for line in blocked_asset_lines], |
| 289 | + "", |
| 290 | + "Emit that corrected mutation tool call now. Do not return an empty response.", |
| 291 | + ] |
| 292 | + ) |
| 293 | + |
| 278 | 294 | if dod is not None: |
| 279 | 295 | quality_repair_message = self._build_quality_repair_empty_retry_message( |
| 280 | 296 | retry_number=retry_number, |
@@ -392,6 +408,77 @@ class ResponseRepairer: |
| 392 | 408 | ] |
| 393 | 409 | ) |
| 394 | 410 | |
| 411 | + def _blocked_html_asset_empty_retry_lines( |
| 412 | + self, |
| 413 | + dod: DefinitionOfDone | None, |
| 414 | + ) -> list[str]: |
| 415 | + messages = list(getattr(self.context.session, "messages", []) or []) |
| 416 | + for message in reversed(messages[-8:]): |
| 417 | + content = str(getattr(message, "content", "") or "") |
| 418 | + if "HTML local asset references do not exist" not in content: |
| 419 | + continue |
| 420 | + missing_assets = _extract_missing_local_asset_hrefs(content) |
| 421 | + if not missing_assets: |
| 422 | + continue |
| 423 | + tool_call = self._tool_call_for_tool_result(message, messages) |
| 424 | + target = self._tool_call_file_target(tool_call) |
| 425 | + if not target: |
| 426 | + preferred_target = self._preferred_retry_target(dod) |
| 427 | + target = str(preferred_target) if preferred_target is not None else "" |
| 428 | + if not target: |
| 429 | + continue |
| 430 | + |
| 431 | + asset_preview = ", ".join(f"`{asset}`" for asset in missing_assets[:3]) |
| 432 | + return [ |
| 433 | + ( |
| 434 | + f"Last blocked HTML mutation target: " |
| 435 | + f"`{display_runtime_path(target)}`." |
| 436 | + ), |
| 437 | + f"Missing local asset href(s): {asset_preview}.", |
| 438 | + ( |
| 439 | + "Retry the same file, but remove the entire stylesheet/image/script " |
| 440 | + "tag that points at those missing hrefs, or create the referenced " |
| 441 | + "asset first before linking it." |
| 442 | + ), |
| 443 | + ( |
| 444 | + "Prefer removing the asset link and inlining necessary styling/content " |
| 445 | + "unless the user explicitly requested a shared asset file." |
| 446 | + ), |
| 447 | + ( |
| 448 | + "Do not include those missing href values in the next `write`, " |
| 449 | + "`edit`, or `patch` content." |
| 450 | + ), |
| 451 | + ] |
| 452 | + return [] |
| 453 | + |
| 454 | + def _tool_call_for_tool_result( |
| 455 | + self, |
| 456 | + tool_result_message: object, |
| 457 | + messages: list[object], |
| 458 | + ) -> ToolCall | None: |
| 459 | + result_ids = { |
| 460 | + str(getattr(result, "tool_call_id", "") or "") |
| 461 | + for result in getattr(tool_result_message, "tool_results", []) or [] |
| 462 | + if str(getattr(result, "tool_call_id", "") or "") |
| 463 | + } |
| 464 | + if not result_ids: |
| 465 | + return None |
| 466 | + for message in reversed(messages): |
| 467 | + for tool_call in getattr(message, "tool_calls", []) or []: |
| 468 | + if str(getattr(tool_call, "id", "") or "") in result_ids: |
| 469 | + return tool_call |
| 470 | + return None |
| 471 | + |
| 472 | + @staticmethod |
| 473 | + def _tool_call_file_target(tool_call: ToolCall | None) -> str: |
| 474 | + if tool_call is None: |
| 475 | + return "" |
| 476 | + return str( |
| 477 | + tool_call.arguments.get("file_path") |
| 478 | + or tool_call.arguments.get("path") |
| 479 | + or "" |
| 480 | + ).strip() |
| 481 | + |
| 395 | 482 | def _build_early_concrete_write_retry_message( |
| 396 | 483 | self, |
| 397 | 484 | dod: DefinitionOfDone, |
@@ -2010,6 +2097,17 @@ def _repair_line_is_html_quality(line: str) -> bool: |
| 2010 | 2097 | return repair_line_is_html_quality(line) |
| 2011 | 2098 | |
| 2012 | 2099 | |
| 2100 | +def _extract_missing_local_asset_hrefs(content: str) -> list[str]: |
| 2101 | + marker = "Missing local asset href(s):" |
| 2102 | + if marker not in content: |
| 2103 | + return [] |
| 2104 | + tail = content.split(marker, 1)[1].strip() |
| 2105 | + target_text = tail.split(". ", 1)[0].strip() |
| 2106 | + if not target_text: |
| 2107 | + return [] |
| 2108 | + return [item.strip() for item in target_text.split(",") if item.strip()] |
| 2109 | + |
| 2110 | + |
| 2013 | 2111 | def _should_encourage_initial_version( |
| 2014 | 2112 | *, |
| 2015 | 2113 | target: Path, |