Improve local verified edit flow
ober
8bd3a828fdea726547f9599c936c1f6ab2dcb808
--- a/main-binary.ss +++ b/main-binary.ss @@ -6,8 +6,10 @@ (jcode core config) (jcode core prompts) (jcode core agent) + (jcode tool web) (jcode ui cli) (jerbsearch engine-worker) + (only (std net uri) uri-parse) (only (std os exec-id) exec-id-resolve exec-id-realpath) (only (std os env) unsetenv)) --- a/src/jcode/core/verified-run.ss +++ b/src/jcode/core/verified-run.ss @@ -41,7 +41,7 @@ (def default-verified-history-keep-batches 3) (def default-local-verified-history-token-limit 24000) (def default-local-verified-history-keep-batches 2) -(def default-local-verified-max-completion-tokens 8192) +(def default-local-verified-max-completion-tokens 4096) (def default-local-verified-repair-max-completion-tokens 4096) (def (opt-get o key) (let ((p (assoc key o))) (and p (cdr p)))) @@ -181,16 +181,24 @@ (def (verified-staged-repair-tool-spec? spec) (member (tool-spec-name spec) - '("line_edit" "replace_def" "replace_range"))) + '("line_edit" "replace_def" "replace_range" "verify"))) (def (verified-rejected-draft-inspection-tool-spec? spec) (member (tool-spec-name spec) '("read" "balance"))) +(def (rejected-draft-staged-repair-mode?) + (and (current-rejected-ss-draft) + (or (> (current-rejected-draft-inspections) + rejected-draft-inspection-limit) + (>= (current-rejected-draft-path-reject-count) + local-expert-after-rejected-drafts)))) + (def (local-focused-repair-mode?) (and (current-verified-local-model?) (or (current-after-failed-verify?) (and (current-rejected-ss-draft) (> (current-rejected-draft-inspections) 0)) + (rejected-draft-staged-repair-mode?) (> (current-successful-edit-count) 0)))) (def (verified-provider-tool-spec spec) @@ -289,9 +297,7 @@ ;; bounded staged edits; another read or full rewrite repeats the ;; expensive failure cycle. ((and (current-verified-local-model?) - (current-rejected-ss-draft) - (> (current-rejected-draft-inspections) - rejected-draft-inspection-limit) + (rejected-draft-staged-repair-mode?) (not (verified-staged-repair-tool-spec? spec))) #f) ((not (verified-mcp-tool-spec? spec)) #t) @@ -311,9 +317,49 @@ ((and (= (current-successful-edit-count) 0) (>= (current-pre-edit-mcp-count) pre-edit-mcp-limit)) #f) - (else #t)))))) + (else #t)))))) specs))) +(def (tool-specs-include? specs name) + (let loop ((xs specs)) + (cond + ((null? xs) #f) + ((string=? (tool-spec-name (car xs)) name) #t) + (else (loop (cdr xs)))))) + +(def (local-force-first-edit-tool-choice specs) + (and (current-verified-local-model?) + (= (current-successful-edit-count) 0) + (not (current-edited-since-verify?)) + (not (current-after-failed-verify?)) + (not (current-rejected-ss-draft)) + (not (current-pending-ss-create-repair)) + (>= (current-pre-edit-inspection-count) 2) + (tool-specs-include? specs "edit") + (openai-function-tool-choice "edit"))) + +(def (local-forced-first-edit-specs specs forced-choice) + (if forced-choice + (filter (lambda (spec) + (member (tool-spec-name spec) '("edit" "write"))) + specs) + specs)) + +(def (local-forced-first-edit-response response forced-choice) + (if (and forced-choice + (pair? response) + (let loop ((xs response)) + (cond + ((null? xs) #f) + ((not (and (wtool-call? (car xs)) + (member (wtool-call-tool (car xs)) + '("edit" "write")))) + #t) + (else (loop (cdr xs)))))) + (make-text-response + "Forced first-edit turn rejected: the model called a hidden inspection or non-edit tool. Do not call read, list, verify, or any other tool on this turn. Call exactly edit(path=<target file>, content=<complete minimal acceptance-complete file body>) or write(path=<target file>, content=<complete minimal acceptance-complete file body>) now.") + response)) + (def (provider-responder provider) (let ((backend (if (procedure? provider) provider @@ -353,7 +399,14 @@ ((make-provider-backend (get-expert-provider) chat-direct-via-stream) messages specs #f)) - (backend messages specs #f)))))))) + (let ((forced-choice (local-force-first-edit-tool-choice specs))) + (parameterize + ((current-tool-choice-override forced-choice)) + (local-forced-first-edit-response + (backend messages + (local-forced-first-edit-specs specs forced-choice) + #f) + forced-choice)))))))))) ;; ── verify oracle ────────────────────────────────────────────────────── (def (tail-lines s n) @@ -1476,6 +1529,16 @@ (not (string=? (string-downcase v) "false")) (not (string=? (string-downcase v) "no"))))) +(def (env-positive-int name) + (let ((v (getenv name))) + (and v + (let ((n (string->number (string-trim v)))) + (and (integer? n) (> n 0) n))))) + +(def (local-verified-first-draft-cap) + (or (env-positive-int "JCODE_LOCAL_VERIFIED_MAX_COMPLETION_TOKENS") + default-local-verified-max-completion-tokens)) + (def (schema-function schema) (and (hash-table? schema) (hash-get schema "function"))) @@ -1679,6 +1742,9 @@ (def current-successful-edit-count (make-parameter 0)) +(def current-last-edit-path + (make-parameter #f)) + (def current-pre-edit-mcp-count (make-parameter 0)) @@ -1734,6 +1800,7 @@ (current-requirements-review-pending? #f) (current-inspections-after-edit 0) (current-successful-edit-count 0) + (current-last-edit-path #f) (current-pre-edit-mcp-count 0) (current-pre-edit-run-alias-count 0) (current-pre-edit-inspection-count 0) @@ -2000,6 +2067,7 @@ (reset-failed-verify-inspections!) (current-pre-edit-run-alias-count 0) (current-successful-edit-count (+ (current-successful-edit-count) 1)) + (current-last-edit-path path) (current-edited-since-verify? #t) (current-inspections-after-edit 0)) @@ -2567,13 +2635,26 @@ (map string-trim (string-split raw #\:))) '())) +(def (env-disabled? name) + (let ((v (getenv name))) + (and v + (let ((s (string-downcase (string-trim v)))) + (or (string=? s "0") + (string=? s "false") + (string=? s "no") + (string=? s "off")))))) + +(def (auto-jerboa-home-lib-enabled?) + (not (env-disabled? "JCODE_AUTO_JERBOA_HOME_LIB"))) + (def (verified-read-roots cwd) (let* ((configured (map (lambda (root) (abs-path cwd (expand-home-path root))) (split-read-roots (getenv "JCODE_READ_ROOTS")))) (jerboa-home (getenv "JERBOA_HOME")) (jerboa-lib - (and jerboa-home + (and (auto-jerboa-home-lib-enabled?) + jerboa-home (not (string=? jerboa-home "")) (string-append (abs-path cwd (expand-home-path jerboa-home)) "/lib")))) @@ -2686,6 +2767,17 @@ (def (missing-edit-args-message) "edit/write requires concrete arguments. Do not retry empty edit() or write(). To create or replace a file, call edit(path=\"life.ss\", content=<complete file contents>). To patch existing code, call edit(path=\"life.ss\", old_str=<exact current text>, new_str=<replacement>) or use line_edit/replace_def/replace_range. Then call verify().") +(def (infer-local-edit-path path content old-str line-no) + (or path + (and (current-verified-local-model?) + (not old-str) + (= line-no 0) + (string? content) + (not (string=? (string-trim content) "")) + (or (current-rejected-draft-path) + (current-last-edit-path) + (scope-primary-file (current-write-scope)))))) + (def (missing-local-mutation-args-message tool call-shape) (string-append (symbol->string tool) @@ -4205,7 +4297,7 @@ (cons "(return)" "Jerboa has no return form; use cond/if/named let to return values") (cons "(break " "Jerboa has no break form; use named let/conditionals") (cons "(continue " "Jerboa has no continue form; use named let/conditionals") - (cons "list-set!" "list-set! is not available in Jerboa; use vectors or rebuild lists") + (cons "list-set!" "list-set! is not available in Jerboa. For mutable grids, make the board and rows vectors and use vector-set! on the row vector; if the data is a list, rebuild the list/row and replace the parent value instead.") (cons "random-make-state" "use (random n); do not use random-make-state") (cons "'." "do not use '. as a datum; use a normal symbol such as 'empty or a number sentinel") (cons "(quote .)" "do not use (quote .); use a normal symbol such as 'empty or a number sentinel") @@ -4729,13 +4821,15 @@ (def (do-edit args cwd) (let* ((content (arg-content args)) - (path (arg-path args #f)) + (path0 (arg-path args #f)) (old-str (or (arg-ref args "old_str" #f) (arg-ref args "old_string" #f))) (new-str (or (arg-ref args "new_str" #f) (arg-ref args "new_string" #f) (and old-str content))) (line-no (arg-int args "line" 0)) + (path (infer-local-edit-path path0 content old-str line-no)) + (inferred-path? (and (not path0) path)) (required-full-rewrite? (required-repair-full-rewrite? cwd path content old-str line-no))) (cond @@ -4884,6 +4978,7 @@ ((and (file-exists? p) (source-ss-path? path) (local-focused-repair-mode?) + (not inferred-path?) (not required-full-rewrite?)) (raise-recoverable-tool-error (string-append @@ -5512,7 +5607,7 @@ (number->string local-pre-edit-inspection-limit) " substantive read/shell/MCP inspections. Do not inspect external Jerboa sources unless one exact API fact blocks the implementation. Write the first complete draft within these budgets. After a failed verify, use at most " (number->string local-inspection-after-failed-verify-limit) - " focused inspections before editing the diagnosed span. Local replace_range edits are transactional: a syntactically broken result is rejected and leaves the on-disk file unchanged.\n") + " focused inspections before editing the diagnosed span. Code written in reasoning, analysis, markdown fences, or prose does not change files and will be discarded; put the implementation directly in an edit/write/line_edit/replace_def/replace_range tool call. For the first implementation draft, use one edit/write tool call with the complete file content instead of drafting the file in reasoning. For larger app/game/TUI tasks, write the smallest acceptance-complete vertical slice first: satisfy the verifier's required modes and source hooks, verify it, then add only behavior the verifier proves is missing. Local replace_range edits are transactional: a syntactically broken result is rejected and leaves the on-disk file unchanged.\n") "") (if run-aliases? (string-append @@ -5645,6 +5740,7 @@ (current-requirements-review-pending? #f) (current-inspections-after-edit 0) (current-successful-edit-count 0) + (current-last-edit-path #f) (current-pre-edit-mcp-count 0) (current-pre-edit-run-alias-count 0) (current-pre-edit-inspection-count 0) @@ -5652,7 +5748,7 @@ (current-verified-local-model? local-model?) (current-max-tokens-cap (and local-model? - default-local-verified-max-completion-tokens)) + (local-verified-first-draft-cap))) (current-verify-failure-count 0) (current-force-expert-next? #f) (current-serving-forced-expert? #f) --- a/src/jcode/core/workflow-runner.ss +++ b/src/jcode/core/workflow-runner.ss @@ -398,7 +398,7 @@ "If code must change, call edit/write/line_edit/replace_def/replace_range with complete concrete code, never placeholders.\n" (if (and (string? raw) (string-prefix? "<think>" (string-trim raw))) - "Your prior turn exhausted its output budget in reasoning. Do not restart or repeat that analysis; use the task and tool results already present and issue the pending edit now.\n" + "Your prior turn exhausted its output budget in reasoning. Code in reasoning was discarded and did not change any file. Do not restart or repeat that analysis; issue exactly one edit/write/line_edit/replace_def/replace_range tool call now with the concrete code in the tool arguments.\n" "") (if (> retry-count 1) "This is a repeated prose-only response. Stop free-form text and issue the tool call now.\n" --- a/src/jcode/provider/provider.ss +++ b/src/jcode/provider/provider.ss @@ -13,6 +13,8 @@ provider-base-url provider-local? current-max-tokens-cap + current-tool-choice-override + openai-function-tool-choice current-stream-abort? model-rejects-tools? extract-text-tool-calls @@ -990,6 +992,19 @@ ;; cloud-origin workflows leave it unbound and are unaffected. (def current-max-tokens-cap (make-parameter #f)) +;; Verified workflows can temporarily require a specific OpenAI-compatible +;; function call after the model has inspected enough context. Leave unbound +;; for ordinary chat and providers that should choose tools normally. +(def current-tool-choice-override (make-parameter #f)) + +(def (openai-function-tool-choice name) + (let ((fn (make-hash-table)) + (choice (make-hash-table))) + (hash-put! fn "name" name) + (hash-put! choice "type" "function") + (hash-put! choice "function" fn) + choice)) + (def (openai-max-tokens provider) (let* ((configured (or (positive-int-value (getenv "JCODE_MAX_TOKENS")) @@ -1551,7 +1566,8 @@ (when (and tools (not (null? tools)) (not (model-rejects-tools? (provider-model provider)))) (hash-put! body "tools" tools) - (hash-put! body "tool_choice" "auto")) + (hash-put! body "tool_choice" + (or (current-tool-choice-override) "auto"))) body)) ;; Some models reject the `tools` parameter entirely (e.g. DeepSeek's @@ -2269,7 +2285,8 @@ (when (and tools (not (null? tools)) (not (model-rejects-tools? (provider-model provider)))) (hash-put! body "tools" tools) - (hash-put! body "tool_choice" "auto")) + (hash-put! body "tool_choice" + (or (current-tool-choice-override) "auto"))) body)) (def (openai-stream-chat provider messages tools token-cb) --- a/test/run.ss +++ b/test/run.ss @@ -3263,10 +3263,11 @@ (check-pred! "verified-run: repeated local rejection focuses staged tools" staged-tool-names (lambda (names) - (and (= (length names) 3) + (and (= (length names) 4) (member "line_edit" names) (member "replace_def" names) - (member "replace_range" names)))) + (member "replace_range" names) + (member "verify" names)))) (check! "verified-run: local staged line edit promotes repaired file" (slurp target-path) (string-append "(import (jerboa prelude))\n" good-line "\n"))) @@ -3434,8 +3435,8 @@ (check! "verified-run: local edit is automatically verified" result "VERIFIED: exit 0\n")) (let ([observed (reverse caps)]) - (check! "verified-run: local first-draft completion cap is 8k" - (car observed) 8192) + (check! "verified-run: local first-draft completion cap is 4k" + (car observed) 4096) (check! "verified-run: local post-verifier repair cap is 4k" (cadr observed) 4096)) (check! "verified-run: automatic verify avoids a provider round" @@ -3443,6 +3444,45 @@ (safe-delete-test-file! target-path)) (let* ([vr-dir "/tmp"] + [target "jcode-local-env-first-cap.txt"] + [target-path (string-append vr-dir "/" target)] + [old-cap (getenv "JCODE_LOCAL_VERIFIED_MAX_COMPLETION_TOKENS")] + [calls 0] + [caps '()] + [responder + (lambda (_messages _tools _step) + (set! calls (+ calls 1)) + (set! caps (cons (current-max-tokens-cap) caps)) + (case calls + [(1) (list (make-wtool-call "verify" '() #f))] + [(2) (list (make-wtool-call "edit" + (list (cons "path" target) + (cons "content" "repaired\n")) #f))] + [else (error 'test "automatic local verify should avoid another provider call")]))]) + (safe-delete-test-file! target-path) + (dynamic-wind + (lambda () (putenv "JCODE_LOCAL_VERIFIED_MAX_COMPLETION_TOKENS" "8192")) + (lambda () + (let ([result + (verified-run responder "repair with a larger local first cap" + (list + (cons 'cwd vr-dir) + (cons 'verify-command (string-append "grep -q repaired " target)) + (cons 'write-scope (parse-write-scope target)) + (cons 'local-model? #t) + (cons 'max-iterations 8)))]) + (check! "verified-run: env local first cap still verifies" + result "VERIFIED: exit 0\n")) + (let ([observed (reverse caps)]) + (check! "verified-run: env local first cap is honored" + (car observed) 8192) + (check! "verified-run: env local repair cap remains 4k" + (cadr observed) 4096))) + (lambda () + (putenv "JCODE_LOCAL_VERIFIED_MAX_COMPLETION_TOKENS" (or old-cap "")) + (safe-delete-test-file! target-path)))) + + (let* ([vr-dir "/tmp"] [target "jcode-local-env-initial-verify.txt"] [target-path (string-append vr-dir "/" target)] [old-initial (getenv "JCODE_VERIFIED_INITIAL_VERIFY")] @@ -3660,8 +3700,8 @@ (check! "verified-run: inspected rejected draft repair verifies" result "VERIFIED: exit 0\n")) (let ([observed (reverse caps)]) - (check! "verified-run: uninspected rejected draft keeps 8k cap" - (cadr observed) 8192) + (check! "verified-run: uninspected rejected draft keeps 4k cap" + (cadr observed) 4096) (check! "verified-run: inspected rejected draft repair uses 4k cap" (caddr observed) 4096)) (check-pred! "verified-run: inspected rejected draft hides broad write" @@ -3671,6 +3711,47 @@ (safe-delete-test-file! target-path)) (let* ([vr-dir "/tmp"] + [target "jcode-local-repeated-rejected-draft-schema.ss"] + [target-path (string-append vr-dir "/" target)] + [bad-a "(import (jerboa prelude))\n(def (main) (displayln \"bad\")))\n"] + [bad-b "(import (jerboa prelude))\n(def (main) (displayln \"still bad\")))\n"] + [tool-names-after-repeat '()] + [calls 0] + [responder + (lambda (_messages tools _step) + (set! calls (+ calls 1)) + (when (= calls 3) + (set! tool-names-after-repeat (map tool-spec-name tools))) + (case calls + [(1) (list (make-wtool-call "write" + (list (cons "path" target) + (cons "content" bad-a)) #f))] + [(2) (list (make-wtool-call "write" + (list (cons "path" target) + (cons "content" bad-b)) #f))] + [(3) (list (make-wtool-call "line_edit" + (list (cons "path" target) + (cons "line" 2) + (cons "content" "(def (main) (displayln \"fixed\"))")) #f))] + [else (error 'test "repeated rejected draft should auto-verify")]))]) + (safe-delete-test-file! target-path) + (let ([result + (verified-run responder "repair repeated rejected draft without another full write" + (list + (cons 'cwd vr-dir) + (cons 'verify-command (string-append "grep -q fixed " target)) + (cons 'write-scope (parse-write-scope target)) + (cons 'local-model? #t) + (cons 'max-iterations 8)))]) + (check! "verified-run: repeated rejected draft schema repair verifies" + result "VERIFIED: exit 0\n")) + (check-pred! "verified-run: repeated rejected draft hides write schema" + tool-names-after-repeat (lambda (names) (not (member "write" names)))) + (check-pred! "verified-run: repeated rejected draft keeps line edit schema" + tool-names-after-repeat (lambda (names) (member "line_edit" names))) + (safe-delete-test-file! target-path)) + + (let* ([vr-dir "/tmp"] [target "jcode-local-rejected-draft-read-after-failure.ss"] [target-path (string-append vr-dir "/" target)] [initial "(import (jerboa prelude))\n(def value \"broken\")\n"] @@ -4288,7 +4369,8 @@ [lib (string-append home "/lib")] [external-file (string-append lib "/runtime-api.ss")] [old-home (getenv "JERBOA_HOME")] - [old-read-roots (getenv "JCODE_READ_ROOTS")]) + [old-read-roots (getenv "JCODE_READ_ROOTS")] + [old-auto-lib (getenv "JCODE_AUTO_JERBOA_HOME_LIB")]) (ensure-test-directory! home) (ensure-test-directory! lib) (write-test-output-file external-file @@ -4311,7 +4393,25 @@ (str-contains? s lib)))))) (lambda () (putenv "JERBOA_HOME" (or old-home "")) - (putenv "JCODE_READ_ROOTS" (or old-read-roots ""))))) + (putenv "JCODE_READ_ROOTS" (or old-read-roots "")))) + (dynamic-wind + (lambda () + (putenv "JERBOA_HOME" home) + (putenv "JCODE_READ_ROOTS" "") + (putenv "JCODE_AUTO_JERBOA_HOME_LIB" "0")) + (lambda () + (let* ([wf (coding-workflow "true" "/tmp/jcode-verified-home-workspace")] + [read-tool (tool-def-callable (workflow-get-tool-def wf "read"))]) + (check-pred! "verified-run: auto Jerboa lib root can be disabled" + (read-tool (list (cons "path" external-file))) + (lambda (s) (str-contains? s "read refused outside"))) + (check-pred! "verified-run: disabled auto Jerboa lib is not advertised" + (workflow-system-prompt-template wf) + (lambda (s) (not (str-contains? s "JERBOA_HOME/lib")))))) + (lambda () + (putenv "JERBOA_HOME" (or old-home "")) + (putenv "JCODE_READ_ROOTS" (or old-read-roots "")) + (putenv "JCODE_AUTO_JERBOA_HOME_LIB" (or old-auto-lib ""))))) (let* ([wf (coding-workflow "true" "/tmp")] [names (workflow-tool-names wf)]) @@ -7827,6 +7927,107 @@ (safe-delete-test-file! target-path))) (let* ([vr-dir "/tmp"] + [target "jcode-verified-infer-rejected-draft-path.ss"] + [target-path (string-append vr-dir "/" target)] + [bad "(import (jerboa prelude))\n(def (main)\n (return 1))\n"] + [good "(import (jerboa prelude))\n(displayln \"fixed\")\n"] + [tool-results '()] + [slurp (lambda (p) (call-with-input-file p (lambda (i) (get-string-all i))))] + [calls 0] + [responder + (lambda (_messages _tools _step) + (set! calls (+ calls 1)) + (case calls + [(1) + (list (make-wtool-call "write" + (list (cons "path" target) + (cons "content" bad)) #f))] + [(2) + (list (make-wtool-call "write" + (list (cons "content" good)) #f))] + [else + (error 'test "path inference should auto-verify after repaired write")]))]) + (safe-delete-test-file! target-path) + (let ([result + (verified-run responder "infer the rejected draft path for a local rewrite" + (list + (cons 'cwd vr-dir) + (cons 'verify-command (string-append "grep -q fixed " target)) + (cons 'write-scope (parse-write-scope target)) + (cons 'local-model? #t) + (cons 'max-iterations 8) + (cons 'max-tool-errors 4) + (cons 'on-message + (lambda (m) + (when (equal? (message-role m) "tool") + (set! tool-results + (cons (message-content m) tool-results)))))))]) + (check! "verified-run: local rewrite infers rejected draft path" + result "VERIFIED: exit 0\n") + (check! "verified-run: inferred rejected path wrote repaired file" + (slurp target-path) good) + (check-pred! "verified-run: missing path repair followed rejected draft" + (reverse tool-results) + (lambda (xs) + (let loop ([ys xs]) + (cond + [(null? ys) #f] + [(and (str-contains? (car ys) "syntax guard rejected") + (str-contains? (car ys) target)) #t] + [else (loop (cdr ys))]))))) + (safe-delete-test-file! target-path)) + + (let* ([vr-dir "/tmp"] + [target "jcode-verified-infer-last-edit-path.ss"] + [target-path (string-append vr-dir "/" target)] + [draft "(import (jerboa prelude))\n(displayln \"draft\")\n"] + [good "(import (jerboa prelude))\n(displayln \"fixed\")\n"] + [calls 0] + [tool-results '()] + [responder + (lambda (_messages _tools _step) + (set! calls (+ calls 1)) + (case calls + [(1) + (list (make-wtool-call "write" + (list (cons "path" target) + (cons "content" draft)) #f))] + [(2) + (list (make-wtool-call "write" + (list (cons "content" good)) #f))] + [else + (list (make-wtool-call "verify" '() #f))]))] + [slurp (lambda (p) (call-with-input-file p (lambda (i) (get-string-all i))))]) + (safe-delete-test-file! target-path) + (let ([result + (verified-run responder "infer the last edit path for a local rewrite" + (list + (cons 'cwd vr-dir) + (cons 'verify-command (string-append "grep -q fixed " target)) + (cons 'write-scope (parse-write-scope target)) + (cons 'local-model? #t) + (cons 'max-iterations 8) + (cons 'max-tool-errors 4) + (cons 'on-message + (lambda (m) + (when (equal? (message-role m) "tool") + (set! tool-results + (cons (message-content m) tool-results)))))))]) + (check! "verified-run: local rewrite infers last edited path" + result "VERIFIED: exit 0\n") + (check! "verified-run: inferred last edited path wrote repaired file" + (slurp target-path) good) + (check-pred! "verified-run: pathless rewrite uses last edited path" + (reverse tool-results) + (lambda (xs) + (let loop ([ys xs]) + (cond + [(null? ys) #f] + [(str-contains? (car ys) "wrote jcode-verified-infer-last-edit-path.ss") #t] + [else (loop (cdr ys))]))))) + (safe-delete-test-file! target-path)) + + (let* ([vr-dir "/tmp"] [target "jcode-verified-rejected-draft-limit.ss"] [target-path (string-append vr-dir "/" target)] [bad "(import (jerboa prelude))\n(define (main)\n (displayln \"draft\")))\n"] @@ -9493,12 +9694,24 @@ (lambda () (serve-one-captured-json! srv captured 200 chat-body) (let* ([p (make-provider "mlx" "" "unit-test-model" base-url)] - [msg (parameterize ([*config* cfg]) + [msg (parameterize ([*config* cfg] + [current-tool-choice-override + (openai-function-tool-choice "lookup")]) (provider-chat p (list (make-user-message "hi")) (list tool)))] - [req (vector-ref captured 0)]) + [req (vector-ref captured 0)] + [req-json (and req (string->json-object req))] + [tool-choice (and req-json + (hashtable-ref req-json "tool_choice" #f))] + [tool-choice-fn + (and (hashtable? tool-choice) + (hashtable-ref tool-choice "function" #f))]) (check! "mlx local tool chat reply" (message-content msg) "ok") (check! "mlx local tool chat includes tools" (and req (str-contains? req "\"tools\"")) #t) + (check! "mlx local tool chat honors tool_choice override" + (and (hashtable? tool-choice-fn) + (hashtable-ref tool-choice-fn "name" #f)) + "lookup") (check! "mlx local tool chat omits logprobs" (and req (str-contains? req "\"logprobs\"")) #f) (check! "mlx local tool chat omits top_logprobs" @@ -10221,6 +10434,12 @@ (slurp-file "main-binary.ss") (lambda (src) (str-find src "\"--no-expert\"" 0))) +(check-pred! "binary roots web URI dependency" + (slurp-file "main-binary.ss") + (lambda (src) + (and (str-find src "(jcode tool web)" 0) + (str-find src "(std net uri)" 0)))) + (check-pred! "built-in CLI help advertises verified iteration budget" (slurp-file "src/jcode/ui/cli.ss") (lambda (src)