Stream sub-agent chat to avoid TUI stalls

ober

55cf7187dfae43b4b878018f543c69250c9aaf57

diff --git a/src/jcode/core/agent.ss b/src/jcode/core/agent.ss
index 76ab25f..5723f8f 100644
--- a/src/jcode/core/agent.ss
+++ b/src/jcode/core/agent.ss
@@ -1476,6 +1476,7 @@ Be concise. Prefer edit over write for modifying existing files.
     (let ((err-port (current-error-port))
           (log-lvl  (current-log-level))
           (tool-cb  (current-tool-cb))
+          (stream-abort (current-stream-abort?))
           (mcp-cb   (current-mcp-call-cb)))
       (let ((threads (map (lambda (tc)
                             (spawn (lambda ()
@@ -1483,6 +1484,7 @@ Be concise. Prefer edit over write for modifying existing files.
                                        ((current-error-port err-port)
                                         (current-log-level log-lvl)
                                         (current-tool-cb tool-cb)
+                                        (current-stream-abort? stream-abort)
                                         (current-mcp-call-cb mcp-cb))
                                        (execute-single-tool tc)))))
                           tool-calls)))
@@ -1623,14 +1625,57 @@ Be concise. Prefer edit over write for modifying existing files.
              (agent-chat-loop-stream provider new-msgs tools (+ round 1)))))))))
 
 (def (agent-chat-messages messages)
-  "Run the non-streaming chat loop over an explicit MESSAGES list (system +
-   history already built by the caller). Returns (values final-content
+  "Run the chat loop over an explicit MESSAGES list (system + history already
+   built by the caller). Returns (values final-content
    final-messages) so callers — the resumable task tool — can persist the
    transcript and continue the same sub-agent conversation later."
   (let ((provider (get-current-provider))
         (tools (get-tool-schemas)))
     (parameterize ((forge-breaker-state (make-forge-breaker-state)))
-      (agent-chat-loop-track provider messages tools 0))))
+      (agent-chat-loop-track-stream provider messages tools 0))))
+
+(def (agent-chat-loop-track-stream provider messages tools round)
+  ;; Task/sub-agent conversations are not rendered token-by-token, but they
+  ;; still need streaming transport. A non-streaming local MLX read can block
+  ;; in the kernel long enough to pin the TUI and make ESC ineffective.
+  (let-values (((content tool-calls usage)
+                (stream-chat-with-expert provider messages tools
+                  (lambda (_tok) (void)))))
+    (let* ((hermes-tcs (if (and (null? tool-calls) (not (string=? content "")))
+                         (try-parse-hermes-tool-calls content)
+                         '()))
+           (effective-tcs (if (null? tool-calls) hermes-tcs tool-calls))
+           (effective-content
+             (cond
+               ((not (null? hermes-tcs))
+                (let ((clean (strip-hermes-blocks content)))
+                  (if (string=? clean "") #f clean)))
+               (else (if (string=? content "") #f content)))))
+      (cond
+        ((null? effective-tcs)
+         (let ((final (make-assistant-message effective-content #f)))
+           (values (or effective-content "")
+                   (append messages (list final)))))
+        ((>= round *max-tool-rounds*)
+         (let* ((response (make-assistant-message effective-content effective-tcs))
+                (results (execute-tool-calls effective-tcs))
+                (new-messages (append messages (list response) results)))
+           (let-values (((fc _tc _u)
+                         (stream-chat-with-expert provider new-messages '()
+                           (lambda (_tok) (void)))))
+             (let ((final (make-assistant-message (if (string=? fc "") #f fc) #f)))
+               (values (or fc "")
+                       (append new-messages (list final)))))))
+        ((forge-no-progress? effective-tcs)
+         (log-warn logger "no-progress-break" `((round . ,round)))
+         (let ((final (make-assistant-message forge-no-progress-message #f)))
+           (values forge-no-progress-message
+                   (append messages (list final)))))
+        (else
+         (let* ((response (make-assistant-message effective-content effective-tcs))
+                (results (execute-tool-calls effective-tcs))
+                (new-messages (append messages (list response) results)))
+           (agent-chat-loop-track-stream provider new-messages tools (+ round 1))))))))
 
 (def (agent-chat-loop-track provider messages tools round)
   (let ((response (chat-with-expert provider messages tools)))