Narrow forced edit retry nudges

ober

de5517d10ed5c03e3f563b056ca5d68626ee983a

diff --git a/src/jcode/core/workflow-runner.ss b/src/jcode/core/workflow-runner.ss
index bffe634..d6e3fa1 100644
--- a/src/jcode/core/workflow-runner.ss
+++ b/src/jcode/core/workflow-runner.ss
@@ -413,6 +413,23 @@
   (and (string? content)
        (string-prefix? forced-first-edit-retry-marker content)))
 
+(def (forced-first-edit-retry-tool-names content fallback-tool-names)
+  (cond
+    ((not (string? content))
+     fallback-tool-names)
+    ((or (string-contains content "required edit")
+         (string-contains content "required write"))
+     '("edit" "write"))
+    ((string-contains content "required balance")
+     '("balance"))
+    ((string-contains content "required replace_range")
+     '("replace_range"))
+    ((string-contains content "required replace_def")
+     '("replace_def"))
+    ((string-contains content "required line_edit")
+     '("line_edit"))
+    (else fallback-tool-names)))
+
 (def (suppress-verified-text-history? workflow retry-text-responses? content)
   (and retry-text-responses?
        (verified-workflow? workflow)
@@ -644,7 +661,10 @@
 	                      (emit! (make-user-message
 	                               (if forced-first-edit-retry
                                      (forced-first-edit-retry-message
-                                       (tool-spec-name-list visible-tool-specs))
+                                       (forced-first-edit-retry-tool-names
+                                         content
+                                         (tool-spec-name-list
+                                           visible-tool-specs)))
 	                                 (text-retry-message
 	                                   workflow enforcer messages content
                                    (error-tracker-consecutive-retries
diff --git a/test/run.ss b/test/run.ss
index 59d619e..311a542 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -1598,6 +1598,36 @@
            #t]
           [else (loop (cdr ys))])))))
 
+(let* ([w (mk-research-wf)]
+       [msgs '()]
+       [resp (scripted-responder
+               (list (make-text-response
+                       "JCODE_INTERNAL_FORCE_FIRST_EDIT_RETRY provider streamed 513 text/reasoning characters without calling a tool while this turn required edit.")
+                     (list (make-wtool-call "search" '() #f))
+                     (list (make-wtool-call "answer" '() #f))))]
+       [result (run-workflow w "go" resp
+                 (list (cons 'max-iterations 6)
+                       (cons 'retry-text-responses? #t)
+                       (cons 'on-message
+                         (lambda (m) (set! msgs (cons m msgs))))))])
+  (check! "forced first-edit detailed retry workflow still reaches terminal"
+          result "ANSWER delivered")
+  (check-pred! "forced first-edit detailed retry narrows nudge to edit tools"
+    (reverse msgs)
+    (lambda (xs)
+      (let loop ([ys xs])
+        (cond
+          [(null? ys) #f]
+          [(and (equal? (message-role (car ys)) "user")
+                (str-contains? (message-content (car ys))
+                               "Currently available structured tools: edit, write")
+                (str-contains? (message-content (car ys))
+                               "Next response should be one structured edit(...) or write(...) tool call.")
+                (not (str-contains? (message-content (car ys))
+                                    "search, lookup, answer")))
+           #t]
+          [else (loop (cdr ys))])))))
+
 ;; Opt-in text retries fail through ToolCallError instead of burning every
 ;; workflow iteration.
 (let* ([w (mk-research-wf)]