agent/tool: harden against off-target edits by local models
ober
d5c064f2817ead6a78c66250ea1e2cb47b890b5b
--- a/src/jcode/core/agent.ss +++ b/src/jcode/core/agent.ss @@ -37,17 +37,20 @@ You have access to these tools: ~a -IMPORTANT: Do NOT call the same tool repeatedly with the same or similar arguments. -If you already retrieved information, use what you have. Use batch to parallelize. -Use glob to find files by pattern instead of exploring directory by directory with ls. +IMPORTANT RULES: +- Source code lives in src/ with extension .ss. NEVER edit files in lib/ — those are generated build artifacts (.sls/.so). +- ALWAYS read a file (with the read tool) before editing it. Do not invent file contents. +- The edit tool requires old_str to match the file BYTE-FOR-BYTE. If edit returns 'old_str not found', do NOT retry with similar text — read the file to see real content, then edit. +- Do NOT call the same tool repeatedly with the same or similar arguments. Use grep/glob to locate code instead of guessing paths. +- Use batch to parallelize independent reads. When the user asks you to do something: -1. Think about what tools you need -2. Use tools to gather information or make changes -3. Report back with results +1. Use grep or glob to FIND the relevant source files in src/ +2. READ those files to see what's actually there +3. Make minimal targeted edits with the edit tool +4. Report back with results -Be concise and helpful. When editing files, make minimal targeted changes. -Prefer using the edit tool over write for modifying existing files." +Be concise. Prefer edit over write for modifying existing files." (current-directory) (mode-label (current-mode)) (format-tool-list) --- a/src/jcode/tool/file.ss +++ b/src/jcode/tool/file.ss @@ -105,17 +105,36 @@ ;;; WRITE ;;; +(def (generated-artifact-error tool path) + ;; Block writes to generated build artifacts so the model can't "fix" the + ;; bug in the compiled output. Returns an error string or #f. + (let ((p (or path ""))) + (cond + ((string-suffix? ".sls" p) + (format "Error: ~a refused — ~a is a generated .sls build artifact. Edit the corresponding .ss file under src/ instead." tool p)) + ((string-suffix? ".so" p) + (format "Error: ~a refused — ~a is a compiled binary. Edit the corresponding .ss source under src/ instead." tool p)) + ((or (string-contains p "/lib/jcode/") + (string-contains p "/lib/std/") + (string-prefix? "lib/" p) + (string-prefix? "./lib/" p)) + (format "Error: ~a refused — ~a is under lib/ (generated). Edit the corresponding source under src/ instead." tool p)) + (else #f)))) + (def (handle-write args) (let ((path (hash-ref args "path" #f)) (content (hash-ref args "content" #f))) (unless path (error 'write "Missing required parameter: path")) (unless content (error 'write "Missing required parameter: content")) (log-debug logger "write" `((path . ,path) (length . ,(string-length content)))) - (let ((dir (path-directory path))) - (when (and dir (not (equal? dir "")) (not (file-exists? dir))) - (mkdir-p dir))) - (write-file-string path content) - (format "Successfully wrote ~a bytes to ~a" (string-length content) path))) + (cond + ((generated-artifact-error "write" path) => (lambda (e) e)) + (else + (let ((dir (path-directory path))) + (when (and dir (not (equal? dir "")) (not (file-exists? dir))) + (mkdir-p dir))) + (write-file-string path content) + (format "Successfully wrote ~a bytes to ~a" (string-length content) path))))) ;;; EDIT ;;; @@ -126,15 +145,18 @@ (unless path (error 'edit "Missing required parameter: path")) (unless old-str (error 'edit "Missing required parameter: old_str")) (log-debug logger "edit" `((path . ,path))) - (if (file-exists? path) - (let* ((content (read-file-string path)) - (new-content (string-replace-first content old-str new-str))) - (if (equal? content new-content) - (format "Error: old_str not found in ~a" path) - (begin - (write-file-string path new-content) - (format "Successfully edited ~a" path)))) - (format "Error: File not found: ~a" path)))) + (cond + ((generated-artifact-error "edit" path) => (lambda (e) e)) + ((not (file-exists? path)) + (format "Error: File not found: ~a" path)) + (else + (let* ((content (read-file-string path)) + (new-content (string-replace-first content old-str new-str))) + (if (equal? content new-content) + (format "Error: old_str not found in ~a" path) + (begin + (write-file-string path new-content) + (format "Successfully edited ~a" path)))))))) (def (string-replace-first str old new) (let ((idx (find-substring str old))) @@ -222,25 +244,28 @@ (unless path (error 'multi-edit "Missing required parameter: path")) (unless edits (error 'multi-edit "Missing required parameter: edits")) (log-debug logger "multi-edit" `((path . ,path))) - (if (not (file-exists? path)) - (format "Error: File not found: ~a" path) - (let loop ((content (read-file-string path)) - (remaining (if (list? edits) edits (list edits))) - (applied 0)) - (if (null? remaining) - (begin - (write-file-string path content) - (format "Successfully applied ~a edit~a to ~a" - applied (if (= applied 1) "" "s") path)) - (let* ((edit (car remaining)) - (old-str (and (hash-table? edit) (hash-get edit "old_str"))) - (new-str (or (and (hash-table? edit) (hash-get edit "new_str")) ""))) - (if (not old-str) - (loop content (cdr remaining) applied) - (let ((new-content (string-replace-first content old-str new-str))) - (if (equal? content new-content) - (format "Error: old_str not found in ~a: ~s" path old-str) - (loop new-content (cdr remaining) (+ applied 1))))))))))) + (cond + ((generated-artifact-error "multi-edit" path) => (lambda (e) e)) + ((not (file-exists? path)) + (format "Error: File not found: ~a" path)) + (#t + (let loop ((content (read-file-string path)) + (remaining (if (list? edits) edits (list edits))) + (applied 0)) + (if (null? remaining) + (begin + (write-file-string path content) + (format "Successfully applied ~a edit~a to ~a" + applied (if (= applied 1) "" "s") path)) + (let* ((edit (car remaining)) + (old-str (and (hash-table? edit) (hash-get edit "old_str"))) + (new-str (or (and (hash-table? edit) (hash-get edit "new_str")) ""))) + (if (not old-str) + (loop content (cdr remaining) applied) + (let ((new-content (string-replace-first content old-str new-str))) + (if (equal? content new-content) + (format "Error: old_str not found in ~a: ~s" path old-str) + (loop new-content (cdr remaining) (+ applied 1)))))))))))) (def (make-multi-edit-schema) (let ((schema (make-hash-table)) @@ -283,11 +308,12 @@ (let* ((path (or path-override (extract-patch-target patch-str))) (content (if (file-exists? path) (read-file-string path) "")) (result (apply-unified-patch content patch-str))) - (if (string? result) - (begin - (write-file-string path result) - (format "Successfully applied patch to ~a" path)) - (format "Error applying patch: ~a" (cdr result)))))) + (cond + ((generated-artifact-error "patch" path) => (lambda (e) e)) + ((string? result) + (write-file-string path result) + (format "Successfully applied patch to ~a" path)) + (#t (format "Error applying patch: ~a" (cdr result))))))) (def (extract-patch-target patch-str) ;; Find "+++ b/path" or "+++ path" line