Constrain local prompt growth
ober
388f968a2574a5f71176c6089d6de57c5780e886
--- a/src/jcode/core/agent.ss +++ b/src/jcode/core/agent.ss @@ -246,6 +246,7 @@ Be concise. Prefer edit over write for modifying existing files. (def *max-tool-rounds* 100) (def *max-tool-lines* 2000) (def *max-tool-bytes* 51200) ;; 50KB +(def *local-max-tool-bytes* 8192) (def (refresh-system-prompt messages) "Replace the leading system message (if any) with a fresh one reflecting @@ -273,25 +274,36 @@ Be concise. Prefer edit over write for modifying existing files. (unless (file-exists? dir) (mkdir dir)) dir)) +(def (tool-output-byte-limit) + "Return the tool-result preview byte cap for the active model. Small local + context budgets need tighter previews so a few reads cannot build a prompt + large enough to stall local prefill." + (let* ((mdl (or (current-model-override) (config-ref "model") "")) + (win (model-context-window mdl))) + (if (and win (<= win 4096)) + *local-max-tool-bytes* + *max-tool-bytes*))) + (def (truncate-tool-output text) "Cap tool output at *max-tool-lines* / *max-tool-bytes*. If truncated, save the full output to ~/.jcode/truncated/ and return a preview + hint." - (let* ((lines (string-split text #\newline)) + (let* ((byte-limit (tool-output-byte-limit)) + (lines (string-split text #\newline)) (total-bytes (string-length text)) (total-lines (length lines))) (if (and (<= total-lines *max-tool-lines*) - (<= total-bytes *max-tool-bytes*)) + (<= total-bytes byte-limit)) text ;; Truncate: keep first N lines up to byte limit (let loop ((remaining lines) (count 0) (bytes 0) (acc '())) (if (or (null? remaining) (>= count *max-tool-lines*) - (>= bytes *max-tool-bytes*)) + (>= bytes byte-limit)) ;; Save full output, return preview + hint (let* ((preview (string-join (reverse acc) "\n")) (file (path-join (truncated-dir) (format "tool-~a.txt" (time-second (current-time))))) - (removed (if (> total-bytes *max-tool-bytes*) + (removed (if (> total-bytes byte-limit) (format "~a bytes" (- total-bytes (string-length preview))) (format "~a lines" (- total-lines count))))) (with-output-to-file file (lambda () (display text))) --- a/src/jcode/core/compaction-strategy.ss +++ b/src/jcode/core/compaction-strategy.ss @@ -85,10 +85,10 @@ ;; ── Shared helpers ────────────────────────────────────────────────── (def (strategy-estimate-tokens messages) - ;; forge _estimate_tokens: sum of content lengths // 4 (content only). - (quotient - (apply + (map (lambda (m) (string-length (or (message-content m) ""))) messages)) - 4)) + ;; Keep the strategy's trigger aligned with the outer compaction gate. + ;; That estimator includes tool-call argument bytes, which matter in + ;; file-heavy coding sessions. + (estimate-message-tokens messages)) (def (distinct-consecutive xs) ;; Append a value whenever it differs from the previously appended one; @@ -247,7 +247,8 @@ "Pick the strategy from jcode.json (compaction.strategy, default tiered), run it against BUDGET tokens, log the phase reached, return the messages." (let* ((name (compaction-block-ref "strategy" "tiered")) - (keep (compaction-block-ref "strategy_keep_recent" 2)) + (keep (compaction-block-ref "strategy_keep_recent" + (if (and budget (<= budget 4096)) 1 2))) (thr (compaction-block-ref "strategy_threshold" 0.75)) (strat (compaction-strategy-by-name name keep thr)) (res (strat messages budget)) --- a/src/jcode/core/models.ss +++ b/src/jcode/core/models.ss @@ -460,11 +460,11 @@ ;; weights leave only ~4 GB under the 32 GB wired cap, so keep the window ;; conservative — compaction must fire before prefill OOMs the GPU. ((string-contains mid "TurboQuant") 16384) - ;; The local qwen3-coder-next MLX server stalls around 26k-token prefill - ;; turns. jcode's message estimator does not include the fixed tool - ;; schema overhead (roughly another 8-9k tokens), so this is an effective - ;; message budget rather than the model's architectural context window. - ((string-contains mid "qwen3-coder-next-mlx") 8192) + ;; The local qwen3-coder-next MLX server stalls around large prefill + ;; turns. jcode's estimator does not include the fixed tool-schema + ;; overhead, so this is an effective working budget rather than the + ;; model's architectural context window. + ((string-contains mid "qwen3-coder-next-mlx") 4096) ;; grok-build: Grok CLI proxy advertises a 512k context window in ;; ~/.grok/models_cache.json (info.context_window). Hardcode here so ;; the TUI ctx% bar has a value even before models_cache.json is read. --- a/test/run.ss +++ b/test/run.ss @@ -779,7 +779,7 @@ (message-thinking-set! m "pondering") (check! "type: reasoning (thinking+empty content)" (message-derived-type m) message-type-reasoning)) -;; estimate = sum(content lengths) // 4 +;; estimate = message content plus tool-call bytes, divided by 4 (check! "estimate-tokens content/4" (strategy-estimate-tokens (list (make-user-message (make-string 400 #\x)))) 100) @@ -812,8 +812,8 @@ (make-assistant-message "final"))) (check! "tier step indices" (derive-step-indices hist-tier) (list #f #f 1 1 2 3 3 4 5 5 6)) (check! "tier eligible-end keep=1" (find-eligible-end hist-tier 1) 10) -;; original estimate 5005//4 = 1251 tokens -(check! "tier original estimate" (strategy-estimate-tokens hist-tier) 1251) +;; original estimate includes three tool-call overheads: (5005 + 90)//4 = 1273 +(check! "tier original estimate" (strategy-estimate-tokens hist-tier) 1273) ;; SlidingWindow keep=1: below threshold → phase 0; above → keep head+protected tail (let* ([s (make-sliding-window 1 0.75)] [r (s hist-tier 10000)]) @@ -1821,7 +1821,7 @@ (check! "ctx window grok-build = 512000" (model-context-window "grok-build") 512000) (check! "ctx window grok-3 unaffected (no clause)" (model-context-window "grok-3") #f) (check! "ctx window qwen3-coder-next mlx conservative" - (model-context-window "/Users/user/models/qwen3-coder-next-mlx") 8192) + (model-context-window "/Users/user/models/qwen3-coder-next-mlx") 4096) (check! "ctx window generic qwen3 remains 32768" (model-context-window "qwen3:8b-q4_K_M") 32768)