diff options
| author | Vineet Kumar <git@vineetk.net> | 2026-05-24 13:50:36 -0400 |
|---|---|---|
| committer | Vineet Kumar <git@vineetk.net> | 2026-05-24 14:10:39 -0400 |
| commit | 6cb91f228a2f0433a7bc93205df8a5f3bd3b2304 (patch) | |
| tree | 19c3fa40a70b8663ea4401f88051eac684e3a500 | |
| parent | d57c422cddf657fab565411ffe9956a32ff37230 (diff) | |
rename from gptel-tools an add gptel-agent's bash tool
| -rw-r--r-- | gptel-tools.el | 61 | ||||
| -rw-r--r-- | llm-tools-hl.el (renamed from gptel-tools-hl.el) | 39 | ||||
| -rw-r--r-- | llm-tools.el | 122 |
3 files changed, 144 insertions, 78 deletions
diff --git a/gptel-tools.el b/gptel-tools.el deleted file mode 100644 index 1e8453c..0000000 --- a/gptel-tools.el +++ /dev/null | |||
| @@ -1,61 +0,0 @@ | |||
| 1 | ;; read_file (hl-file-read) | ||
| 2 | (gptel-make-tool | ||
| 3 | :name "read_file" | ||
| 4 | :function 'hl-file-read | ||
| 5 | :description "\ | ||
| 6 | Read an entire file and return its contents in hashline format. Each | ||
| 7 | line is represented as LINE_NUMBER HASH|CONTENT, where LINE_NUMBER is | ||
| 8 | the 1-based line number, HASH is a 2-character content-derived bigram, | ||
| 9 | and CONTENT is the verbatim line text. Example output: | ||
| 10 | |||
| 11 | 1aa|const char *TITLE = \"Mr\"; | ||
| 12 | 2ab|void greet(char *name) { | ||
| 13 | 3cd| return name; | ||
| 14 | |||
| 15 | The LINE_NUMBER and HASH together form an anchor (e.g., 1aa, 2ab) that | ||
| 16 | uniquely identifies a line by position and content. Copy anchors | ||
| 17 | verbatim from the output when referencing lines in edit operations. Do | ||
| 18 | not fabricate or modify hashes. Call this tool before editing any file | ||
| 19 | to obtain current content and valid anchors. Re-call if anchors no | ||
| 20 | longer match after external changes." | ||
| 21 | :args (list '(:name "file" | ||
| 22 | :type string | ||
| 23 | :description "Path to the file to read, relative to the working directory.")) | ||
| 24 | :async nil | ||
| 25 | :confirm nil | ||
| 26 | :include t) | ||
| 27 | |||
| 28 | ;; TODO write_file (with-temp-buffer + insert) | ||
| 29 | ;; TODO edit_file (hashline + patch apply) | ||
| 30 | |||
| 31 | ;; TODO bash_readonly (shell-command-to-string, whitelisted commands) | ||
| 32 | ;; TODO bash (shell-command-to-string, needs heavy filtering) | ||
| 33 | |||
| 34 | ;; find_files (directory-files-recursively) | ||
| 35 | (gptel-make-tool | ||
| 36 | :name "find_files" | ||
| 37 | :function (lambda (dir &optional pattern) | ||
| 38 | (let ((git-dir (concat (file-name-as-directory dir) ".git"))) | ||
| 39 | (delq nil (mapcar (lambda (f) | ||
| 40 | (unless (string-prefix-p git-dir f) f)) | ||
| 41 | (directory-files-recursively dir (or pattern "")))))) | ||
| 42 | :description "\ | ||
| 43 | Recursively find all files in a given directory, excluding files within | ||
| 44 | .git directories. Returns a list of absolute file paths. Optionally | ||
| 45 | accepts a pattern to filter file names (e.g., `*.el' for Emacs Lisp | ||
| 46 | files). Use this to discover files in a project directory before reading | ||
| 47 | or editing them." | ||
| 48 | :args (list '(:name "directory" | ||
| 49 | :type string | ||
| 50 | :description "Path to the directory to be read, relative to the working directory.") | ||
| 51 | '(:name "pattern" | ||
| 52 | :type string | ||
| 53 | :description "Optional file name pattern to filter results, e.g. `*.el' or `*.txt'." | ||
| 54 | :optional t)) | ||
| 55 | :async nil | ||
| 56 | :confirm nil | ||
| 57 | :include t) | ||
| 58 | |||
| 59 | ;; TODO grep (just grep :D) | ||
| 60 | ;; TODO fetch_url (use gptel-agent's one as a base, but needs mime handling like PDFs and images) | ||
| 61 | ;; TODO web_search (use gptel-agent's eww/shr method as a base) | ||
diff --git a/gptel-tools-hl.el b/llm-tools-hl.el index fd08260..c682334 100644 --- a/gptel-tools-hl.el +++ b/llm-tools-hl.el | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | (defvar hl-bigrams | 1 | (defvar llm-tools--hl-bigrams |
| 2 | (list "aa" "ab" "ac" "ad" "ae" "af" "ag" "ah" "ai" "aj" "ak" "al" "am" "an" | 2 | (list "aa" "ab" "ac" "ad" "ae" "af" "ag" "ah" "ai" "aj" "ak" "al" "am" "an" |
| 3 | "ao" "ap" "aq" "ar" "as" "at" "au" "av" "aw" "ax" "ay" "az" "ba" "bb" | 3 | "ao" "ap" "aq" "ar" "as" "at" "au" "av" "aw" "ax" "ay" "az" "ba" "bb" |
| 4 | "bc" "bd" "be" "bf" "bg" "bh" "bi" "bj" "bk" "bl" "bm" "bn" "bo" "bp" | 4 | "bc" "bd" "be" "bf" "bg" "bh" "bi" "bj" "bk" "bl" "bm" "bn" "bo" "bp" |
| @@ -54,10 +54,10 @@ vocabulary, unlike a hash's hex digits. | |||
| 54 | Taken from https://github.com/can1357/oh-my-pi. | 54 | Taken from https://github.com/can1357/oh-my-pi. |
| 55 | Precisely: https://raw.githubusercontent.com/can1357/oh-my-pi/85003ca/packages/coding-agent/src/hashline/bigrams.json") | 55 | Precisely: https://raw.githubusercontent.com/can1357/oh-my-pi/85003ca/packages/coding-agent/src/hashline/bigrams.json") |
| 56 | 56 | ||
| 57 | (defun hl-hash (line) | 57 | (defun llm-tools--hl-hash (line) |
| 58 | (elt hl-bigrams (% (sxhash line) 647))) | 58 | (elt llm-tools--hl-bigrams (% (sxhash line) 647))) |
| 59 | 59 | ||
| 60 | (defun hl-file-lines (file &optional beg end) | 60 | (defun llm-tools--hl-file-lines (file &optional beg end) |
| 61 | "Return FILE contents as a list of strings. | 61 | "Return FILE contents as a list of strings. |
| 62 | With optional BEG and END (inclusive, 1-based), return the lines between that range." | 62 | With optional BEG and END (inclusive, 1-based), return the lines between that range." |
| 63 | (with-temp-buffer | 63 | (with-temp-buffer |
| @@ -67,36 +67,41 @@ With optional BEG and END (inclusive, 1-based), return the lines between that ra | |||
| 67 | (cl-subseq lines (1- beg) end) | 67 | (cl-subseq lines (1- beg) end) |
| 68 | lines)))) | 68 | lines)))) |
| 69 | 69 | ||
| 70 | (defun hl-format-lines (lines &optional start include-content?) | 70 | (defun llm-tools--hl-format-lines (lines &optional start include-content?) |
| 71 | "Format LINES in hashline format. | 71 | "Format LINES in hashline format. |
| 72 | START is 1-based (default 1). | 72 | START is 1-based (default 1). |
| 73 | If INCLUDE-CONTENT is non-nil, append `|LINE' after each hash." | 73 | If INCLUDE-CONTENT is non-nil, append `|LINE' after each hash." |
| 74 | (string-join | 74 | (string-join |
| 75 | (cl-mapcar (lambda (line i) | 75 | (cl-mapcar (lambda (line i) |
| 76 | (if include-content? | 76 | (if include-content? |
| 77 | (format "%d%2s|%s" i (hl-hash line) line) | 77 | (format "%d%2s|%s" i (llm-tools--hl-hash line) line) |
| 78 | (format "%d%2s" i (hl-hash line)))) | 78 | (format "%d%2s" i (llm-tools--hl-hash line)))) |
| 79 | lines | 79 | lines |
| 80 | (number-sequence (or start 1) (+ (or start 1) (1- (length lines))))) | 80 | (number-sequence (or start 1) (+ (or start 1) (1- (length lines))))) |
| 81 | "\n")) | 81 | "\n")) |
| 82 | 82 | ||
| 83 | (defun hl-file-read (file) | 83 | (defun llm-tools--hl-file-read (file) |
| 84 | "Return FILE contents in hashline format." | 84 | "Return FILE contents in hashline format." |
| 85 | (hl-format-lines (hl-file-lines file) nil t)) | 85 | (llm-tools--hl-format-lines (llm-tools--hl-file-lines file) nil t)) |
| 86 | 86 | ||
| 87 | (defun hl-file-read-range (file beg end) | 87 | (defun llm-tools--hl-file-read-range (file beg end) |
| 88 | "Return FILE contents in hashline format from lines BEG to END." | 88 | "Return FILE contents in hashline format from lines BEG to END." |
| 89 | (hl-format-lines (hl-file-lines file beg end) beg t)) | 89 | (llm-tools--hl-format-lines (llm-tools--hl-file-lines file beg end) beg t)) |
| 90 | 90 | ||
| 91 | ;; FIXME might be redundant with =hl-file-line-hash= if I'm only | 91 | ;; FIXME might be redundant with =llm-tools--hl-file-line-hash= if I'm only |
| 92 | ;; verifying the hash of the anchors and telling LLM to re-read the | 92 | ;; verifying the hash of the anchors and telling LLM to re-read the |
| 93 | ;; file when they don't match | 93 | ;; file when they don't match |
| 94 | (defun hl-file-read-hashes (file) | 94 | (defun llm-tools--hl-file-read-hashes (file) |
| 95 | "Return FILE contents in hashline format without content." | 95 | "Return FILE contents in hashline format without content." |
| 96 | (hl-format-lines (hl-file-lines file))) | 96 | (llm-tools--hl-format-lines (llm-tools--hl-file-lines file))) |
| 97 | 97 | ||
| 98 | (defun hl-file-line-hash (file n) | 98 | (defun llm-tools--hl-file-line-hash (file n) |
| 99 | "Return the hash of the Nth line in FILE." | 99 | "Return the hash of the Nth line in FILE." |
| 100 | (let ((lines (hl-file-lines file))) | 100 | (let ((lines (llm-tools--hl-file-lines file))) |
| 101 | (hl-hash (elt lines (1- n))))) | 101 | (llm-tools--hl-hash (elt lines (1- n))))) |
| 102 | 102 | ||
| 103 | ;; before applying patch, first read the file to see if there were any changes | ||
| 104 | ;; if the llm's replacement text's hashlines do not match current hashlines, reject the request | ||
| 105 | ;; maybe if it just shifted a little bit (since the anchor is line number and hash of the line's content), there can be some kind of autohealing? | ||
| 106 | ;; rather, the llm would not format it like the reads. it's just going to provide text according to what the anchors it gives say | ||
| 107 | ;; so I just need to verify if the anchors didn't change | ||
diff --git a/llm-tools.el b/llm-tools.el new file mode 100644 index 0000000..6476d15 --- /dev/null +++ b/llm-tools.el | |||
| @@ -0,0 +1,122 @@ | |||
| 1 | ;; read_file (hl-file-read) | ||
| 2 | (gptel-make-tool | ||
| 3 | :name "read_file" | ||
| 4 | :function 'llm-tools--hl-file-read | ||
| 5 | :description "\ | ||
| 6 | Read an entire file and return its contents in hashline format. Each | ||
| 7 | line is represented as LINE_NUMBER HASH|CONTENT, where LINE_NUMBER is | ||
| 8 | the 1-based line number, HASH is a 2-character content-derived bigram, | ||
| 9 | and CONTENT is the verbatim line text. Example output: | ||
| 10 | |||
| 11 | 1aa|const char *TITLE = \"Mr\"; | ||
| 12 | 2ab|void greet(char *name) { | ||
| 13 | 3cd| return name; | ||
| 14 | |||
| 15 | The LINE_NUMBER and HASH together form an anchor (e.g., 1aa, 2ab) that | ||
| 16 | uniquely identifies a line by position and content. Copy anchors | ||
| 17 | verbatim from the output when referencing lines in edit operations. Do | ||
| 18 | not fabricate or modify hashes. Call this tool before editing any file | ||
| 19 | to obtain current content and valid anchors. Re-call if anchors no | ||
| 20 | longer match after external changes." | ||
| 21 | :args '((:name "file" | ||
| 22 | :type string | ||
| 23 | :description "Path to the file to read, relative to the working directory.")) | ||
| 24 | :async nil | ||
| 25 | :confirm nil | ||
| 26 | :include t) | ||
| 27 | |||
| 28 | ;; TODO write_file (with-temp-buffer + insert) | ||
| 29 | ;; TODO edit_file (hashline + patch apply) | ||
| 30 | |||
| 31 | ;; TODO bash_readonly (shell-command-to-string, whitelisted commands) | ||
| 32 | ;; TODO compile command (calls build system, doesn't need confirmation, whitelisted commands) | ||
| 33 | |||
| 34 | ;; bash (make-process, taken from gptel-agent) | ||
| 35 | (defun llm-tools--execute-bash (callback command) | ||
| 36 | "Execute COMMAND asynchronously in bash and call CALLBACK with output. | ||
| 37 | |||
| 38 | CALLBACK is called with the command output string when the process finishes. | ||
| 39 | COMMAND is the bash command string to execute." | ||
| 40 | (let* ((output-buffer (generate-new-buffer " *llm-tools-bash*")) | ||
| 41 | (proc (make-process | ||
| 42 | :name "llm-tools-bash" | ||
| 43 | :buffer output-buffer | ||
| 44 | :command (list "bash" "-c" command) | ||
| 45 | :connection-type 'pipe | ||
| 46 | :sentinel | ||
| 47 | (lambda (process _event) | ||
| 48 | (when (memq (process-status process) '(exit signal)) | ||
| 49 | (let* ((exit-code (process-exit-status process)) | ||
| 50 | (output (with-current-buffer (process-buffer process) | ||
| 51 | (buffer-string)))) | ||
| 52 | (kill-buffer (process-buffer process)) | ||
| 53 | (funcall callback | ||
| 54 | (if (zerop exit-code) | ||
| 55 | output | ||
| 56 | (format "Command failed with exit code %d:\nSTDOUT+STDERR:\n%s" | ||
| 57 | exit-code output))))))))) | ||
| 58 | proc)) | ||
| 59 | |||
| 60 | (gptel-make-tool | ||
| 61 | :name "Bash" | ||
| 62 | :function #'llm-tools--execute-bash | ||
| 63 | :description "Execute Bash commands. | ||
| 64 | |||
| 65 | This tool provides access to a Bash shell with GNU coreutils (or | ||
| 66 | equivalents) available. Use this to inspect system state, run builds, | ||
| 67 | tests or other development or system administration tasks. | ||
| 68 | |||
| 69 | Do NOT use this for file operations, finding, reading or editing files. | ||
| 70 | Use the provided file tools instead: `read_file`, `write_file`, | ||
| 71 | `edit_file`, `find_file`, `grep`. | ||
| 72 | |||
| 73 | - Quote file paths with spaces using double quotes. | ||
| 74 | - Chain dependent commands with && (or ; if failures are OK) | ||
| 75 | - Use absolute paths instead of cd when possible | ||
| 76 | - For parallel commands, make multiple `Bash` calls in one message | ||
| 77 | - Run tests, check your work or otherwise close the loop to verify changes you make. | ||
| 78 | |||
| 79 | EXAMPLES: | ||
| 80 | - Find recent errors: 'grep -i error /var/log/app.log | tail -20' | ||
| 81 | - Check file type: 'file document.pdf' | ||
| 82 | - Count lines: 'wc -l *.txt' | ||
| 83 | |||
| 84 | The command will be executed in the current working directory. Output is | ||
| 85 | returned as a string. Long outputs should be filtered/limited using pipes." | ||
| 86 | :args '(( :name "command" | ||
| 87 | :type string | ||
| 88 | :description "The Bash command to execute. \ | ||
| 89 | Can include pipes and standard shell operators. | ||
| 90 | Example: 'ls -la | head -20' or 'grep -i error app.log | tail -50'")) | ||
| 91 | :confirm t | ||
| 92 | :include t | ||
| 93 | :async t) | ||
| 94 | |||
| 95 | ;; find_files (directory-files-recursively) | ||
| 96 | (gptel-make-tool | ||
| 97 | :name "find_files" | ||
| 98 | :function (lambda (dir &optional pattern) | ||
| 99 | (let ((git-dir (concat (file-name-as-directory dir) ".git"))) | ||
| 100 | (delq nil (mapcar (lambda (f) | ||
| 101 | (unless (string-prefix-p git-dir f) f)) | ||
| 102 | (directory-files-recursively dir (or pattern "")))))) | ||
| 103 | :description "\ | ||
| 104 | Recursively find all files in a given directory, excluding files within | ||
| 105 | .git directories. Returns a list of absolute file paths. Optionally | ||
| 106 | accepts a pattern to filter file names (e.g., `*.el' for Emacs Lisp | ||
| 107 | files). Use this to discover files in a project directory before reading | ||
| 108 | or editing them." | ||
| 109 | :args '((:name "directory" | ||
| 110 | :type string | ||
| 111 | :description "Path to the directory to be read, relative to the working directory.") | ||
| 112 | (:name "pattern" | ||
| 113 | :type string | ||
| 114 | :description "Optional file name pattern to filter results, e.g. `*.el' or `*.txt'." | ||
| 115 | :optional t)) | ||
| 116 | :async nil | ||
| 117 | :confirm nil | ||
| 118 | :include t) | ||
| 119 | |||
| 120 | ;; TODO grep (just grep :D) | ||
| 121 | ;; TODO fetch_url (use gptel-agent's one as a base, but needs mime handling like PDFs and images) | ||
| 122 | ;; TODO web_search (use gptel-agent's eww/shr method as a base) | ||
