Most Emacs LLM integrations turn the editor into a mouth: you type, a model replies into a buffer, and that is the interaction. El Yayster inverts that relationship entirely. Instead of Emacs talking to a model, the model inhabits Emacs. The agent perceives your live editing environment and acts on it through gated Emacs Lisp calls. Decide, call a tool, read the result, repeat, all against any OpenAI-compatible /chat/completions endpoint. Local Ollama is the default path, and no cloud service is required.

The name and the thesis

The project name is both a joke and a statement. El is the definite article, the Yayster, and it is also el as in Emacs Lisp. The rest of the name is the dare: a carefree mindset that looks at eval and M-x and says yes, seat the model there too. The agent drafts; you still commit.

One file, no package dependencies

The entire project is a single file, yayster.el. There are no ELPA dependencies beyond what ships with Emacs itself. The only external requirements are curl on your PATH and a reachable model endpoint. On macOS, curl is already built in. On Linux, apt install curl covers it. The project requires Emacs 28.1 or later.

Installation is straightforward. Drop the file somewhere on disk, add the directory to your load-path, require the module, and call yayster-awaken. You can bind yayster-step to a key for asking questions and yayster-use-host to switch between model endpoints. The default endpoint targets Ollama on localhost at port 11434 with the model qwen3:8b.

(add-to-list 'load-path "/path/to/yayster.el")
(require 'yayster)
(yayster-awaken)

(global-set-key (kbd "C-c y") #'yayster-step)
(global-set-key (kbd "C-c Y") #'yayster-use-host)

(setq yayster-endpoint "http://localhost:11434/v1/chat/completions"
      yayster-model "qwen3:8b")

What the agent actually sees

Every turn, the system prompt feeds the model a live snapshot of your environment: which buffers are open, their major modes, the current working directory, and a manifest of available tools. The agent is not guessing about your context. It knows it is inside Emacs and is told exactly what it can perceive and do there.

Seven capabilities are registered. Three are read-only and auto-approve: read_buffer pulls text from any buffer, list_buffers enumerates open buffers with their modes, and apropos plus describe let the agent explore Emacs's own self-documentation. These are the mechanisms for self-discovery; the agent can learn what functions exist and what they do without any human intervention.

Four capabilities are mutating and always prompt for permission before execution. eval_elisp evaluates arbitrary Emacs Lisp expressions. write_buffer replaces a buffer's contents. run_command executes a shell command. The permission prompt shows the full argument payload with no truncation, so a rebind to y-or-n-p is never a blind approval. On a graphical frame, you get Allow, YOLO (which stops asking for the rest of the session), and Deny. On a terminal, the options are [y]/[n]/[!]. C-g or cancel counts as deny.

Non-blocking and observable

Requests run in a curl subprocess, so Emacs stays responsive while the model is thinking. Progress streams into a dedicated *yayster* buffer. The header line of that buffer shows the live model name, context-window occupancy (auto-detected from Ollama's /api/show endpoint), and per-turn and session token counts. There is no polling loop; the display refreshes via advice on turn start, turn finish, host switch, abort, and safety re-arm.

An optional add-on, yayster-mode-line.el, installs the current host, model, and a pulse animation into mode-line-format. If a turn hangs, yayster-abort kills the request, unlinks temp files, and clears the busy flag.

The ReAct protocol under the hood

The agent follows a text-based ReAct loop. It emits an ACTION: line and an ARGS: line, each starting on its own line, then reads the tool result. A FINAL: line ends the turn and is never executed, even if an ACTION pair is also present in the same response. Malformed JSON in ARGS does not count as a tool call. This keeps the loop deterministic and avoids accidental execution of partial output.

The architecture has five pieces, all in one file. A capabilities registry holds the gated Elisp functions. A permission gate distinguishes read-only from mutating actions. A situating prompt constructs the live environment and tool manifest. An agent loop drives the ReAct cycle against the configured endpoint. And yayster-awaken seats the whole thing at startup.

Security means reading what you approve

Read-only tools auto-approve and can still send any open buffer, including ones containing secrets, to the model. That is the feature, and it is also the footgun. The gate only protects you if you actually read what you are approving. run_command enforces a timeout of yayster-command-timeout seconds (default 30) and caps output at yayster-command-max-bytes to prevent runaway processes from flooding the buffer.

For hosted API endpoints, if you set yayster-api-key, the bearer token is written to a temp file with mode 0600 and passed to curl via -H @FILE. The token never appears in a ps listing.

Tested across local models

The project has been tested with Qwen and GLM models served through Ollama, with no code changes between them. Any OpenAI-compatible /chat/completions endpoint works. The README includes a batch-mode test command via ERT:

emacs -Q --batch -L . -l yayster.el -l test/yayster-tests.el \
  -f ert-run-tests-batch-and-exit

The project describes itself as a v0, deliberately small and easy to read end to end. Planned directions include native JSON-Schema tool calling as an alternative to the ReAct text protocol, token-level streaming into *yayster*, automatic host failover, and an org-roam-backed memory system for recall as a knowledge graph. The license is MIT.