<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>operationnull.com</title><id>https://operationnull.com/feeds/tags/llm.xml</id><subtitle>Tag: llm</subtitle><updated>2026-04-24T13:06:47Z</updated><link href="https://operationnull.com/feeds/tags/llm.xml" rel="self" /><link href="https://operationnull.com" /><entry><title>Local LLMs as the glue for complex Emacs functions</title><id>https://operationnull.com/local-llms-as-the-glue-for-complex-emacs-functions.html</id><author><name>bdunahu</name></author><updated>2026-04-05T20:58:00Z</updated><link href="https://operationnull.com/local-llms-as-the-glue-for-complex-emacs-functions.html" rel="alternate" /><content type="html">&lt;p&gt;I do all of my &lt;a href=&quot;https://codeberg.org/bdunahu/kolwynia&quot;&gt;development&lt;/a&gt; on a laptop, so running sophisticated models locally isn't really an option for me.&lt;/p&gt;&lt;p&gt;I've run small models with &lt;code&gt;llama-cpp&lt;/code&gt;, the &lt;code&gt;Qwen4B&lt;/code&gt; model, and &lt;a href=&quot;https://github.com/karthink/gptel&quot;&gt;&lt;code&gt;gptel&lt;/code&gt;&lt;/a&gt;, an LLM client for emacs. In that time, I hadn't considered it useful for any tasks.&lt;/p&gt;&lt;p&gt;But then I realized; &lt;code&gt;gptel&lt;/code&gt; supports tools written using &lt;code&gt;elisp&lt;/code&gt;!&lt;/p&gt;&lt;p&gt;This post is about my initial experiments with the function, since I am trying to figure out if I can integrate agents a little more into my workflow. I tried my first tool from &lt;code&gt;gptel&lt;/code&gt;'s README:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elisp&quot;&gt;(gptel-make-tool
 :name &amp;quot;read_buffer&amp;quot;
 :function (lambda (buffer)
             (unless (buffer-live-p (get-buffer buffer))
               (error &amp;quot;error: buffer %s is not live.&amp;quot; buffer))
             (with-current-buffer  buffer
               (buffer-substring-no-properties (point-min) (point-max))))
 :description &amp;quot;given a buffer name, returns its contents.&amp;quot;
 :args (list '(:name &amp;quot;buffer&amp;quot;
                     :type string
                     :description &amp;quot;the name of the buffer whose contents are to be
retrieved&amp;quot;))
 :category &amp;quot;emacs&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That function allows the model to read the contents of any open file, terminal, browser tab, etc. given its exact name. Even the Qwen4B model was able to answer questions like: &amp;quot;What is in &lt;code&gt;name-of-buffer&lt;/code&gt;&amp;quot;. Of course, I had to supply the name of the buffer it would pass into the function. That hardly is useful to me!&lt;/p&gt;&lt;h1&gt;Inferring Correct Buffer given Mode&lt;/h1&gt;&lt;p&gt;It becomes an &amp;quot;agent&amp;quot; when it's able to make a decision which would be hard to do programatically. So I introduced a new tool, which wrapped a function I made to return a list of buffer names given a &lt;code&gt;major-mode&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elisp&quot;&gt;(defun bd/get-buffers-of-mode (mode)
  &amp;quot;Returns a list of buffers matching mode. This has few uses outside of a
gptel tool.&amp;quot;
  (let ((buffers '()))
    (dolist (buf (buffer-list) buffers)
      (with-current-buffer buf
        (when (eq mode major-mode)
          (push buf buffers))))))

(gptel-make-tool
   :name &amp;quot;get_buffers_of_major_mode&amp;quot;
   :function (lambda (mode)
               (mapconcat (lambda (buf) (buffer-name buf))
                          (bd/get-buffers-of-mode (intern mode))
                          &amp;quot;, &amp;quot;))
   :description &amp;quot;Given the name of an emacs mode (i.e., org-mode), returns a
comma-delimited list of buffer names.&amp;quot;
   :args (list '(:name &amp;quot;mode&amp;quot;
                       :type string
                       :description &amp;quot;the name of an emacs major mode to search
for&amp;quot;))
   :category &amp;quot;emacs&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now I could ask:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;I remember I have a buffer in &lt;code&gt;markdown-mode&lt;/code&gt; which talked about experimenting with creating tools for emacs &lt;code&gt;gptel&lt;/code&gt;. What date was it created?&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;The buffer is named &lt;code&gt;playing_with_gptel_tools.md&lt;/code&gt; and the date listed in the contents is &lt;strong&gt;2026-04-05 20:58&lt;/strong&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;With a function that reads the value of &lt;code&gt;recentf-list&lt;/code&gt;, it's very likely a similar function would work for files which have been closed, and are not in a buffer! You could have a function which returns the most recent file names from the list, then one that, when given a file, opens it in the background and reads the contents.&lt;/p&gt;&lt;h1&gt;Logging Tool Use&lt;/h1&gt;&lt;p&gt;I decided, for debugging purposes as well as safety, I would like to log all the tool calls the agent makes. I didn't see any sort of function provided by &lt;code&gt;gptel&lt;/code&gt; that would let me do this.&lt;/p&gt;&lt;p&gt;So, I introduced the following code, and rewrote my current tools:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elisp&quot;&gt;(defvar bd/llama-cpp-tools-buffer-name &amp;quot;*llama-cpp-tools*&amp;quot;)
(defmacro log-tool-use (name f)
  &amp;quot;Call F with ARGS, logging the call to BD/LLAMA-CPP-TOOLS-BUFFER-NAME.&amp;quot;
  `(lambda (&amp;amp;rest args)
     (let ((str (format &amp;quot;Used tool %s with args: %S\n&amp;quot; ,name args)))
       (with-current-buffer (get-buffer-create
                             bd/llama-cpp-tools-buffer-name)
         (goto-char (point-max))
         (insert str))
       (apply ,f args))))

(gptel-make-tool
 :name &amp;quot;read_buffer&amp;quot;
 :function (log-tool-use
            &amp;quot;read_buffer&amp;quot;
            (lambda (buffer)
              ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The previous query, run again, prints to the log buffer:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Used tool get_buffers_of_major_mode with args: (&amp;quot;markdown-mode&amp;quot;)
Used tool read_buffer with args: (&amp;quot;playing_with_gptel_tools.md&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;h1&gt;Querying Emacs Documentation&lt;/h1&gt;&lt;p&gt;Here, I often need to look up example usages of emacs functions after reading the docs. Being the self-documenting text editor, it makes sense if the LLM knows how to use it too!:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elisp&quot;&gt;(gptel-make-tool
 :name &amp;quot;describe_function_or_variable&amp;quot;
 :function (log-tool-use
            &amp;quot;describe_function_or_variable&amp;quot;
            (lambda (symbol-str)
              (let* ((symbol (intern symbol-str))
                     (found (cl-some (lambda (x)
                                       (funcall (nth 1 x)
                                                symbol))
                                     describe-symbol-backends)))
                (unless found
                  (error &amp;quot;error: symbol %s does not exist.&amp;quot; symbol-str))
                (string-join
                 (delq nil
                       (list
                        (when (fboundp symbol)
                          (documentation symbol t))
                        (when (boundp symbol)
                          (documentation-property symbol
                                                  'variable-documentation
                                                  t))
                        (when (facep symbol)
                          (face-documentation symbol))))
                 &amp;quot;\n--\n&amp;quot;))))
 :description &amp;quot;Given the name of a symbol (which could be a function,
variable, or face), returns its documentation.&amp;quot;
 :args (list '(:name &amp;quot;mode&amp;quot;
                     :type string
                     :description &amp;quot;the name of the symbol to be searched&amp;quot;))
 :category &amp;quot;emacs&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;&lt;p&gt;Show me how to use 'seq-reduce to find the longest string in a list? Don't make a function&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;Here's an example of how to use &lt;code&gt;seq-reduce&lt;/code&gt; to find the longest string in a list:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elisp&quot;&gt;(seq-reduce (lambda (current longest)
            (if (string-length current) (if (&amp;gt; (string-length current) (string-length longest)) current longest) longest))
          '(&amp;quot;apple&amp;quot; &amp;quot;banana&amp;quot; &amp;quot;cherry&amp;quot; &amp;quot;date&amp;quot;))&lt;/code&gt;&lt;/pre&gt;&lt;/blockquote&gt;&lt;p&gt;The main issue is that &lt;code&gt;string-length&lt;/code&gt; doesn't exist, and there's no INITIAL argument. So I made another tool to allow it to evaluate elisp expressions in a container:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elisp&quot;&gt;(defun bd/gptel-run-command-in-container (packages command)
  (shell-command-to-string (concat &amp;quot;guix shell --container --network &amp;quot;
                                   (string-join packages &amp;quot; &amp;quot;) &amp;quot; -- &amp;quot;
                                   command)))

(gptel-make-tool
 :name &amp;quot;execute_elisp_expression&amp;quot;
 :function (bd/gptel-log-tool-use
            &amp;quot;execute_elisp_expression&amp;quot;
            (lambda (expr)
              (bd/gptel-run-command-in-container
               (list &amp;quot;emacs&amp;quot;)
               (concat &amp;quot;emacs -Q --batch --eval \&amp;quot;(princ (progn &amp;quot;
                       expr &amp;quot;))\&amp;quot;&amp;quot;))))
 :description &amp;quot;Execute an arbitrary emacs command and return the result.
Error messages from malformed expressions are returned.&amp;quot;
 :args (list '(:name &amp;quot;expression&amp;quot;
                     :type string
                     :description &amp;quot;One or more sexps to execute in an empty
emacs session.&amp;quot;))
 :category &amp;quot;execute&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I tried a lot of prompts; while it was able to lookup documentation, it didn't really seem to be able to use it to solve interpreter errors, rather than bashing it's head against the wall!&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Use 'seq-reduce to find the longest string in a list, and then test a small example using the tool (no function). You can lookup documentation for functions you are confused about. Do not respond until you have something that you have verified.&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;The issue is that &lt;code&gt;seq-reduce&lt;/code&gt; requires a function that takes two arguments, but the lambda function is being called with three. Let's fix the lambda function to take two arguments and test it again.&lt;/p&gt;&lt;p&gt;&amp;lt;off to work with the tool...&amp;gt;&lt;/p&gt;&lt;p&gt;I apologize for the repeated errors. The issue seems to be with the use of &lt;code&gt;seq-reduce&lt;/code&gt; in this context. Let me try a different approach using &lt;code&gt;reduce&lt;/code&gt; instead.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;It didn't really weave querying the documentation and trying code together as I had hoped, rather going down a rabbit hole with non-existant functions. I stopped it after around 20 iterations. My code to run stuff in a container also has a bug with escaping being applied twice; but it's safe to say coding needs a lot of parts and a well-trained agent!&lt;/p&gt;&lt;p&gt;At least, it knows what emacs is capable of, if asked directly:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;What does the &lt;code&gt;reduce&lt;/code&gt; function do?&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;I will describe the &lt;code&gt;reduce&lt;/code&gt; function for you.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;reduce&lt;/code&gt; function is not a built-in Emacs function. It is part of the Lisp programming language and is used to apply a binary function cumulatively to the elements of a sequence, reducing the sequence to a single value. If you are using Emacs Lisp, you may need to load a package or define it yourself. Let me know if you need further clarification!&lt;/p&gt;&lt;/blockquote&gt;&lt;h1&gt;Playing Music&lt;/h1&gt;&lt;p&gt;I already have a function which uses &lt;code&gt;yt-dlp&lt;/code&gt; to rip a search query (or url) off of youtube. It takes a search query. I originally tried the tool with args:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-elisp&quot;&gt;...
:name &amp;quot;rip_something_from_yt&amp;quot;
:function (log-tool-use
           &amp;quot;rip_something_from_yt&amp;quot;
           rip-video)
:description &amp;quot;Play a video from youtube matching a string&amp;quot;
:args (list '(:name &amp;quot;unique-query&amp;quot;
                    :type string
                    :description &amp;quot;The query to search youtube with. The first
video matching the query will play. Do not search generic queries.&amp;quot;)
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The model's reasoning found it got stuck on a error... I forgot to pass it as a function rather than a variable (&lt;code&gt;#'rip-video&lt;/code&gt;)!:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Okay, the user asked to play a rock song. I used the rip_something_from_yt function with &amp;quot;rock song&amp;quot; as the query and set foreground? to false. The response was &amp;quot;void-variable rip-video&amp;quot;. Hmm, that's not a standard response. Maybe there was an error or the function didn't work as expected.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;But afterwards, it continuously ignored the hint I gave about passing in unique queries, and just passed in the generic &amp;quot;rock song&amp;quot; when asked to play a rock song.&lt;/p&gt;&lt;p&gt;But I have a large directory of local songs on my computer, with sub-directories named after each artist. So I created a tool called &lt;code&gt;get-user-artist-preferences&lt;/code&gt; which calls a shell command to find files in the directory and return a list of artists...&lt;/p&gt;&lt;p&gt;Now a command like this works!:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Can you play a rock song I would probably like?&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote&gt;&lt;p&gt;I played &amp;quot;In Flames - The Wound&amp;quot; for you. Would you like to try another song from your preferred rock/ metal list, or is there a specific band you'd like to hear?&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;That song title doesn't exist by the way, but the youtube search algorithm still prioritizes a song. Sometimes it still doesn't work:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Used tool rip_something_from_yt with args: (&amp;quot;rock song from an artist I would like&amp;quot; :json-false)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That gave me &lt;a href=&quot;https://youtube.com/watch?v=vISSxIDytI0&quot;&gt;this&lt;/a&gt;, which I've never heard before, so I don't mind. The point of the tool was a little randomness anyways!&lt;/p&gt;</content></entry></feed>