bots-garden/mini-mepublic Fork
⑂ v0.1.6
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. d511266 · on v0.1.6 · k33g · Sep 18, 2026
lsp-tool.md · 39 lines · 5.5 KBmarkdown
Blame HistoryOpen raw

The lsp tool: why one tool, and why a symbol field — explanation

What is this about?

Until 2026-09-18 the model knew a project through bash (grep, ls, go build) and the file tools. That is how a human without an editor works, and it is slow and error-prone for the questions an editor answers instantly: where is this defined, who calls it, what type is this, what does the compiler say. Language servers answer those questions for every mainstream language over one protocol, the Language Server Protocol (LSP). Ticket 17 asked to give mm a client for it: configured servers, a built-in tool, documentation with system-prompt samples, and a warning rather than a failure when a server is missing.

Why one tool with an action field

Six requests were wanted: definition, references, hover, document symbols, diagnostics, rename. They could have been six tools. They are one, lsp, with an action enum, for the model this agent targets — a 12B model that reads a tool list of five entries reliably and a list of eleven less so. One tool also carries one description, and the description is where the workflow lives: "references before you change a shared symbol, diagnostics after you edit". Six descriptions would have said it six times or not at all.

Why a symbol field, and positions tried in order

LSP requests take a position: zero-based line and UTF-16 column. The first end-to-end run showed the model sending column: 0 on every call — even when the question gave the column — and looping on the same error. Asking a small model to count characters is asking for a number it cannot produce. So the tool takes the identifier instead: symbol: greet. The client finds its whole-word occurrences in the file and tries them in order until the server answers something; occurrences on comment lines are tried last, because a name is usually mentioned in the comment above its declaration and the server has nothing to say about a comment. The second run, with symbol, answered the same question in two calls with the right lines. Exact line and column remain accepted, 1-based like read_file numbered=true, for the rare model that has them.

The same idea governs the answers: every location comes back as path:line:column <the source line>. The model does not need to open the file to know what a hit is, so it does not — one call instead of three.

Why the files are synced from disk before every request

An editor tells its server about every keystroke. mm has no keystrokes: the model edits with edit_file, and the file on disk is the truth. So before every request the client reads the file and sends its content — didOpen the first time, a whole-document didChange when it moved since — and the server always answers on what the model just wrote. Nothing else has to remember to notify anyone.

Why rename writes the files itself, and asks first

A rename is a WorkspaceEdit: ranges to replace in several files. The client applies them to disk — from the end of each file backwards so earlier positions stay valid, with UTF-16 columns converted to bytes — then re-syncs the changed files so the next question is answered on the renamed code. In the terminal it runs like edit_file does; where a front end has a permission dialog (an editor over ACP, the window mode with permissions: ask) it asks, because it writes, while the five navigation actions do not: a dialog on every read teaches people to click "allow" without reading.

Why a missing server is a warning

A configuration travels with a project; the machines it runs on differ. A server named in the YAML but absent from the PATH is reported once at start-up and left out of the tool's description; when none is found, the tool is not declared at all and the banner says so. The agent starts either way. Declaring a server the tool cannot start would have handed the model a tool that always fails — the silent-failure trap the whole project is built against.

Rejected alternatives

  • One tool per action. Rejected for the model's sake: a longer tool list, six descriptions, the workflow scattered.
  • A hand-rolled JSON-RPC client. Considered, to keep the dependency count at zero. The user chose go.lsp.dev/protocol and go.lsp.dev/jsonrpc2: typed requests and results, the union types the protocol is full of already decoded. The price is two direct dependencies (plus go.uber.org/zap-free v1 packages' own few) and a Go 1.26 floor, which those modules require.
  • Letting the model count columns. Rejected by evidence, see above.
  • Starting every configured server at launch. Rejected: a Python server on a Go project would run for nothing. Servers start on the first call for a file they serve and stop when mm exits.
  • Feeding diagnostics to the model on its own. Rejected: the model asks for them when the skill and the prompt say to (after an edit). Pushing them unasked would put text in the history the model did not request and cannot place.

How it relates to the rest

internal/lsp is the client: server lifecycle, file sync, the six operations, the text shaping, the rename applied to disk. internal/tools declares the tool over it and decides what is printed or emitted as events, like the other tools. internal/config holds lsp.servers and refuses two servers on one extension. main.go checks the PATH, prints the warnings, and stops the servers at exit. The recap line gained a 🔎 N lsp call(s) counter, kept apart from ⚙ commands for the same reason 📝 file op(s) is: asking the compiler is not running a command.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# The `lsp` tool: why one tool, and why a `symbol` field — explanation

## What is this about?

Until 2026-09-18 the model knew a project through `bash` (grep, ls, go build) and the file tools. That is how a human without an editor works, and it is slow and error-prone for the questions an editor answers instantly: where is this defined, who calls it, what type is this, what does the compiler say. Language servers answer those questions for every mainstream language over one protocol, the Language Server Protocol (LSP). Ticket 17 asked to give `mm` a client for it: configured servers, a built-in tool, documentation with system-prompt samples, and a warning rather than a failure when a server is missing.

## Why one tool with an `action` field

Six requests were wanted: definition, references, hover, document symbols, diagnostics, rename. They could have been six tools. They are one, `lsp`, with an `action` enum, for the model this agent targets — a 12B model that reads a tool list of five entries reliably and a list of eleven less so. One tool also carries one description, and the description is where the workflow lives: "references before you change a shared symbol, diagnostics after you edit". Six descriptions would have said it six times or not at all.

## Why a `symbol` field, and positions tried in order

LSP requests take a position: zero-based line and UTF-16 column. The first end-to-end run showed the model sending `column: 0` on every call — even when the question gave the column — and looping on the same error. Asking a small model to count characters is asking for a number it cannot produce. So the tool takes the identifier instead: `symbol: greet`. The client finds its whole-word occurrences in the file and tries them in order until the server answers something; occurrences on comment lines are tried last, because a name is usually mentioned in the comment above its declaration and the server has nothing to say about a comment. The second run, with `symbol`, answered the same question in two calls with the right lines. Exact `line` and `column` remain accepted, 1-based like `read_file numbered=true`, for the rare model that has them.

The same idea governs the answers: every location comes back as `path:line:column  <the source line>`. The model does not need to open the file to know what a hit is, so it does not — one call instead of three.

## Why the files are synced from disk before every request

An editor tells its server about every keystroke. `mm` has no keystrokes: the model edits with `edit_file`, and the file on disk is the truth. So before every request the client reads the file and sends its content — `didOpen` the first time, a whole-document `didChange` when it moved since — and the server always answers on what the model just wrote. Nothing else has to remember to notify anyone.

## Why `rename` writes the files itself, and asks first

A rename is a `WorkspaceEdit`: ranges to replace in several files. The client applies them to disk — from the end of each file backwards so earlier positions stay valid, with UTF-16 columns converted to bytes — then re-syncs the changed files so the next question is answered on the renamed code. In the terminal it runs like `edit_file` does; where a front end has a permission dialog (an editor over ACP, the window mode with `permissions: ask`) it asks, because it writes, while the five navigation actions do not: a dialog on every read teaches people to click "allow" without reading.

## Why a missing server is a warning

A configuration travels with a project; the machines it runs on differ. A server named in the YAML but absent from the `PATH` is reported once at start-up and left out of the tool's description; when none is found, the tool is not declared at all and the banner says so. The agent starts either way. Declaring a server the tool cannot start would have handed the model a tool that always fails — the silent-failure trap the whole project is built against.

## Rejected alternatives

- **One tool per action.** Rejected for the model's sake: a longer tool list, six descriptions, the workflow scattered.
- **A hand-rolled JSON-RPC client.** Considered, to keep the dependency count at zero. The user chose `go.lsp.dev/protocol` and `go.lsp.dev/jsonrpc2`: typed requests and results, the union types the protocol is full of already decoded. The price is two direct dependencies (plus `go.uber.org/zap`-free v1 packages' own few) and a Go 1.26 floor, which those modules require.
- **Letting the model count columns.** Rejected by evidence, see above.
- **Starting every configured server at launch.** Rejected: a Python server on a Go project would run for nothing. Servers start on the first call for a file they serve and stop when `mm` exits.
- **Feeding diagnostics to the model on its own.** Rejected: the model asks for them when the skill and the prompt say to (after an edit). Pushing them unasked would put text in the history the model did not request and cannot place.

## How it relates to the rest

`internal/lsp` is the client: server lifecycle, file sync, the six operations, the text shaping, the rename applied to disk. `internal/tools` declares the tool over it and decides what is printed or emitted as events, like the other tools. `internal/config` holds `lsp.servers` and refuses two servers on one extension. `main.go` checks the `PATH`, prints the warnings, and stops the servers at exit. The recap line gained a `🔎 N lsp call(s)` counter, kept apart from `⚙ commands` for the same reason `📝 file op(s)` is: asking the compiler is not running a command.