bots-garden/mini-mepublic Fork
⑂ feature/response-request
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. 2d0776dUnverified · on feature/response-request · k33g · 15d ago
tools.md · 245 lines · 22.4 KBmarkdown
Blame HistoryOpen raw

Reference: built-in tools

Neutral description of the tools declared to the model. A tool never returns a Go error for an ordinary failure: the failure text is returned to the model as the tool's output. Every tool records its call in the loop detector; the same tool, input and output three times in a row appends [LOOP_DETECTED: …] to the output.

Permissions. "Permission requested" below applies when yolo: false (see configuration), in every front end: the line mode's 🔐 prompt, the window's box, the editor's dialog. With yolo: true (the default) the same calls run without asking. In -p and mm --ask a call that would ask is rejected. A refusal returns [<tool> rejected by the user] ([command rejected by the user] for bash) to the model.

bash

Declared when bashTool: true (the default).

Field Type Description
command string The shell command to run.
Aspect Behaviour
Shell bash -c <command>, a new shell per call, no stdin.
Working directory The process directory in terminal mode; the session cwd in ACP mode.
Timeout 30 s; the output then ends with [error: 30s timeout exceeded].
Cancellation The command belongs to the turn: cancelling the turn (Esc, Ctrl-C, /abort, or asking something else) kills it, and the output ends with [error: cancelled].
Background jobs The output is collected through a temporary file, not a pipe, so the tool returns as soon as the shell exits even when the command left a job running (nohup serve &). The job keeps running — that is usually the point — and writes into a file nobody reads. Through a pipe, os/exec waits for every process holding the write end, so such a command blocked the turn for ever.
Very large output Read back from both ends (1 MiB each) before maxOutput truncation, with [... output too large to read back in full ...] between them.
Output stdout and stderr merged, truncated to maxOutput characters.
Non-zero exit Output ends with [exit code: exit status N].
Terminal display 🛠️ bash: <command> before running (then the 🔐 prompt when permission is requested), then up to previewLines lines dimmed and indented, or (no output).
ACP Tool call of kind execute; permission requested before running; a refusal returns [command rejected by the user] with status failed.
Window mode One ‣ bash <command> line with status, time and folded output. Permission requested: waits for a/s/r/n; a refusal returns the same text as in ACP.

read_skill

Declared only when skillsDir holds at least one skill (<name>.md or <name>/SKILL.md).

Field Type Description
name string Skill name, exactly as listed in the tool description.
Aspect Behaviour
Description Fixed sentence followed by the catalogue: one <name> — <description> line per skill, sorted by name.
Output The full markdown of <skillsDir>/<name>.md, or of <skillsDir>/<name>/SKILL.md when there is no flat file, truncated to maxOutput.
Unknown name No skill named "x". Available skills: a, b, c
Path safety Only the base name of name is used; no directory traversal.
Terminal display 📖 read_skill: <name>; the content is not echoed.
ACP Tool call of kind read; no permission dialog.

read_file

Declared when editTools: true.

Field Type Default Description
path string — File path, relative to the working directory.
start int 0 First line (1-based); 0 = from the start.
end int 0 Last line (1-based, inclusive); 0 = to the end.
numbered bool false Prefix each line with its number.
Aspect Behaviour
Missing file <path> does not exist. Create it with write_file
Output The lines, truncated to maxOutput; the final newline is kept only when the end of the file is shown.
ACP Kind read, with the absolute path as location.

write_file

Declared when editTools: true.

Field Type Description
path string File path, relative to the working directory; parent directories are created.
content string The whole content of the file.
Aspect Behaviour
Existing file Entirely replaced.
Identical content Nothing is written; the result says so.
Output A one-line headline and the diff against the previous content.
ACP Kind edit; permission requested; the change travels as a diff (new-file diff when created).

edit_file

Declared when editTools: true.

Field Type Default Description
path string — Path of an existing file.
edits array of {old, new} — Replacements, all resolved against the original file.
edits[].old string — Exact text to find, present exactly once. Whitespace is forgiven — see How old is matched.
edits[].new string — Replacement; empty deletes the matched text.
dry_run bool false Report what would change, write nothing.
Rule Consequence when broken
old appears exactly once Refused: not found, or ambiguous — old text appears N times (lines 11, 20) — to change ONE of them, include its surrounding lines in old until the text is unique; to change several, send one edit per place….
Two edits must not overlap, nest or repeat Refused.
Edits are resolved against the original text, never in cascade —
Any refused edit Nothing is written.
Aspect Behaviour
Output A one-line headline (edits applied, lines added and deleted, first changed line, and · N matched ignoring whitespace when any edit needed a relaxed match) and the diff with line numbers.
ACP Kind edit; permission requested; the change travels as a diff with old and new text.

How old is matched

Three passes, strictest first; the first pass that finds anything at all wins, so a file that contains the text verbatim is never matched any other way.

Pass What it forgives Reported as
1. Exact Nothing. old is searched byte for byte. —
2. Edges The whitespace around old: an indent the model kept on a single line, a leading or trailing newline. The same padding comes off new when new carries it, so a new that deliberately adds a blank line still adds it. matched ignoring whitespace
3. Indent Every line's leading and trailing whitespace, for an old spanning two lines or more. This is the tabs-versus-spaces case: a model retypes a Go block with four spaces where the file has a tab. matched ignoring whitespace

Two things are not relaxed:

  • Uniqueness. A loose match must still be the only one. Two of them are refused exactly as two exact ones are, with the same message and the same line numbers — that rule is what makes an edit verifiable.
  • The file's indentation. Before writing, new is re-indented to the file's own base indent and unit, so a tab-indented file does not end up with a block of spaces in the middle of it. A blank line stays empty rather than becoming a line of spaces.

The headline then ends with · N matched ignoring whitespace. It is reported rather than hidden: a model told its text did not match exactly writes the next one better.

When nothing matches, the refusal shows the region of the file that came closest, with real line numbers — half the lines have to line up before an excerpt is offered, since pointing at an innocent part of the file is worse than saying nothing:

main.go: edit #1: old text not found — it must match the file exactly, including indentation and line breaks:
  │ func main() {
  │     count := 2
  │     print(count)
  │ }
the closest the file comes to it — copy these lines:
  │ 3 │ func main() {
  │ 4 │ 	count := 1
  │ 5 │ 	print(count)
  │ 6 │ }

lsp

Declared when lsp.servers lists at least one server whose command is on the PATH at start-up. A listed server that is not found is skipped with a [warning: lsp server <name> (<command>) not found on the PATH — skipped]; when none is found the tool is not declared and the banner says so.

Field Type Default Description
action string — definition, references, hover, symbols, diagnostics or rename.
path string — File path, relative to the working directory. Its extension picks the server.
symbol string "" The identifier, as written in the code. Stray quotes and punctuation are cut off and a receiver. or pkg. prefix dropped (Greet', and person.Greet both mean Greet). Its occurrences in the file are tried in order — code lines first, comment lines last — until the server answers. For definition, references, hover, rename.
line int 0 1-based line. With symbol: restricts the search to that line. With column: the exact position.
column int 0 1-based column, in characters, pointing inside the identifier. Only needed without symbol.
new_name string "" The new identifier, for rename. Cleaned like symbol. When it is empty and symbol holds several words (Greet 1 12 Hello — a small model writing symbol, line, column and new name into one string), the first identifier is the symbol and the last the new name.
Aspect Behaviour
Description Names the servers and their extensions (Servers available: gopls (.go), …) and the six actions.
definition, references One path:line:column <source line> per location, sorted by path then position, duplicates removed; paths inside the project are relative, others absolute. references is prefixed with N reference(s): and includes the declaration.
hover The server's text as it sent it (signature, type, documentation; Markdown for most servers).
symbols One <kind> <name> [<detail>] line N per symbol, children indented two spaces.
diagnostics The file is synced, then the call waits up to 3 s for the server's publication. N diagnostic(s) in <path>: then one <severity> path:line:column <message> [<source>] per finding; no diagnostics: <path> is clean; (as last published) when the file did not change since the server last spoke; no diagnostics received for <path> within 3s (…) when it never did.
rename Applies the server's edits to the files on disk (UTF-16 positions converted), tells the server the new contents, and answers renamed to <new>: N edit(s) in M file(s) followed by one path: N edit(s) line per file. Without new_name: [lsp: rename needs new_name — call lsp again with {"action": "rename", "path": …, "symbol": …, "new_name": "<the new identifier>"}; do not use edit_file for a rename].
Positions 1-based lines and columns at the tool's boundary, 0-based UTF-16 on the wire. A position past the end of a line or of the file is refused with the count.
File sync Before every request the file is read from disk and sent as didOpen (first time) or a whole-document didChange (when its content moved): edits made with edit_file are seen without any extra step.
Server lifecycle Started on the first call for one of its extensions, in the project root (the directory mm runs in), kept for the session, stopped (shutdown, exit, then the process) when mm exits. A server that died is started again on the next call.
Failures Always text, never a Go error: [lsp: no language server for "x.md" — configured: gopls (.go)], [lsp: "foo" not found in main.go], [lsp: column 40 is past the end of line 3 (15 characters)], [lsp: gopls: hover failed: …], [lsp: no definition found for the symbol at main.go:5:4].
Terminal display 🔎 lsp: definition main.go greet (or …:9:14 with a position, rename main.go greet → hello), then the answer echoed like a command's output.
ACP / window Kind read for navigation, no permission; rename is kind edit and asks for permission like edit_file ([rename rejected by the user] when refused).
Counter 🔎 N lsp call(s) in the recap line (see below).

fetch_content

Declared only with fetchContentTool: true. Downloads a web page and returns it as Markdown, or saves the Markdown to a file. Package internal/webmd.

Field Type Default Description
url string — The page. A missing scheme means https:// (k33g.org); a scheme typed without its colon is repaired (https//k33g.org). Only http and https.
output string "" File to save the Markdown to, relative to the working directory (the session cwd in ACP); parent directories are created. When set, the Markdown is not returned.
baseUrl string page URL Absolute URL against which relative links and image sources are resolved. Default: the page's final URL, after redirections.
tables string html Tables with rowspan/colspan or a nested table: html keeps them as raw HTML, flatten makes a GFM table and copies a spanned cell into every slot it covers.
strikethrough bool false <del>, <s>, <strike> → ~~text~~; otherwise the plain text.
taskList bool false A list item starting with a checkbox → - [ ] … / - [x] …; otherwise the checkbox is dropped.
Aspect Behaviour
Request GET with User-Agent: mm/<version>; at most 10 redirections; 30 s for the whole fetch; pages above 5 MiB refused.
robots.txt Read (and cached 24 h per origin) before the page and before every redirection target, for the product token mm, else the * group. Allow/Disallow, longest match wins, Allow wins a tie, * and a final $ supported (RFC 9309). A 4xx robots.txt allows everything; a 5xx or an unreachable one forbids everything.
Accepted types text/html, application/xhtml+xml; no Content-Type → sniffed.
Encoding In order: byte-order mark, charset of the Content-Type header, <meta charset> or <meta http-equiv="Content-Type"> in the first 4 KiB, UTF-8 if the bytes are valid UTF-8, else windows-1252. An unknown name is skipped. The content is converted to UTF-8.
Removed script, style, noscript, template, meta, link, base, title, svg, canvas, iframe, object, embed, nav, aside, dialog, button, select, textarea, inputs other than checkboxes, a footer outside article/main, HTML comments, ARIA roles navigation banner complementary search contentinfo dialog, aria-hidden="true", hidden, class/id words ad ads advert sponsor promo cookie consent gdpr newsletter subscribe popup modal share social breadcrumb nav navbar navigation menu sidebar skiplink (and a few variants), classes sr-only visually-hidden screen-reader-text. An element matched by class/id is kept when it contains main, article or h1.
Attributes All removed except a[href], img[src,alt], td/th[colspan,rowspan], ol[start], input[type,checked], and the language-*/lang-* words of a pre/code class.
Conversion h1–h6 → #–######; p → paragraph, blank line between blocks; strong/b → **…**; em/i → *…*; inline code/kbd/samp/tt → backtick span; pre → fenced block (info string from language-x), fence longer than any backtick run inside; br → hard break \; hr → ---; blockquote → > ; a → [text](url), the URL when there is no text, text only for javascript:; img → ![alt](src), ![](src) without alt, alt only for a data: URI; ul/ol → - / N. (from start), nested lists indented by the parent marker's width; simple tables → GFM, first row as header, `
Unknown element Tags dropped, text kept; one that contains a block element is treated as a container.
Empty element Nothing, no blank line.
Invalid HTML Repaired by the HTML5 parser (golang.org/x/net/html), then converted.
Escaping Backslash, *, backtick, [, ], <, and a _ not inside a word get a backslash; so does a line starting with #, >, -, +, =, | or N..
Result The Markdown, ending with exactly one newline; no title, front matter or preamble. Truncated to maxOutput characters.
With output [fetch_content: saved N lines of Markdown (B bytes) from <url> to <file> — the content is not shown].
Page with no content left [fetch_content: <url> has no text content once scripts, navigation and ads are removed — nothing saved]; no file written.
Failure [fetch_content: error N (<class>) — <reason>], see the codes below.
Terminal display 🌐 fetch_content: <url> (→ <file> with output), spinner Fetching, then the result echoed like a command's output (only the confirmation line with output).
ACP / window Kind fetch. No permission without output; with output, permission requested like write_file ([fetch_content rejected by the user] when refused), and the file's absolute path as location.
Code Class Cause
1 conversion Not an HTML page, larger than 5 MiB, undecodable, or the output file cannot be written.
2 input Host unreachable, timeout, HTTP status ≥ 300 after redirections, more than 10 redirections, robots.txt forbids the page or cannot be read.
3 usage Empty or unparsable url, a scheme other than http/https, unknown tables value, baseUrl not absolute.

ask_agent

Declared when mmConfigs names at least one agent. Hands a task to one of them and returns its answer as the tool's result: the model decides when, from the agents' descriptions. Same machinery as /ask (package internal/ask): the agent runs as a child mm -acp, started on its first call and reused, with a fresh session per call.

Field Type Default Description
agent string — A name of mmConfigs.
prompt string — The task, complete and self-contained.
with_context bool false Send this conversation before the prompt: the history the turn started from (questions, answers, one line per tool call, never tool outputs), capped at maxOutput characters, newest kept.
Aspect Behaviour
Description A fixed paragraph, then Agents: and one <name> — <purpose> line per agent, sorted by name. <purpose> is the entry's description, else the agent file's acp.title (unless it is the built-in one), else (no description).
Permission The call itself is gated like bash: requested when yolo: false.
The asked agent's calls Decided by its entry's yolo in mmConfigs when set (true run, false asked), else by this agent's yolo.
Working directory The process directory in the terminal and the window; the session cwd in ACP.
Display The asked agent's text, thoughts and tool calls stream through the turn, its tool calls titled <agent> · <title>. The window names it while it works: header mm [<acp.title>] · <model> @ <provider> · asked by <this agent>, its answer under ‣ mm [<acp.title>] (its mmConfigs name when it has no title). Line mode: 🗣 ask_agent: <agent> (<provider>/<model>) — <first line of the prompt>, then the agent's output.
Watchdog Each event of the asked agent (text, thought, tool call, answered permission) counts as a sign of life for this agent's watchdog. Silence longer than watchdogTimeout — a model loading, a long prompt being read — still ends the turn.
Result The answer, truncated to maxOutput. The tool call's own output in the window and the editor is <agent> answered (N characters): the answer has already streamed. Not added to the history as a /ask exchange: it is a tool result.
Errors [ask_agent: unknown agent "x" — agents: a, b]; [ask_agent: the prompt is empty — say what <agent> must do]; [ask_agent: <agent> failed: <reason>]; [ask_agent: <agent> gave no answer]; a cancelled or broken run returns what was answered, then [ask_agent: <agent> stopped before finishing: <reason>].
Depth An asked agent may itself have mmConfigs and call ask_agent, up to two levels (MM_ASK_DEPTH); beyond, the call fails with the depth message.

Loop detection

Every tool records (tool, input, output) in the loop detector after each call.

Situation Behaviour
The same triple 3 times in a row The tool's output gets [LOOP_DETECTED: You have attempted the same action multiple times with the same result. Please try a different approach or provide more information.] appended. Nothing stops.
3 more identical calls after that (6 in a row) The tool returns an error instead of text and the turn ends: [error: the model repeated the same edit_file call 6 times in a row with the same failing result — turn stopped by the loop detector; rephrase the request, or fix what the tool kept reporting]. The history keeps everything that ran; the error is recorded in the session file's errors array (where: generation).
A different call The count starts again. /new resets the detector; /compact does not.
A tool call whose streamed arguments repeat a pattern (the last 256 characters are one 4–64-character pattern repeated) or exceed 256 KB The generation is stopped at once: [error: the model's tool call ran away: its arguments repeat the same pattern (N characters so far: …) — generation stopped; rephrase the request, or try another model]. Without it a derailed model composes garbage for minutes while the screen shows "Thinking" (the watchdog is fed by every delta). While a tool call streams, the spinner says Composing a tool call.

The thresholds (3, then 3 more) are set in main.go; maxTurns remains the outer bound of a turn.

Counters shown after a turn

Counter Counts
⚙ N command(s) bash tool responses in the turn.
📝 N file op(s) read_file, write_file, edit_file responses.
📖 N skill(s) read_skill responses.
🔎 N lsp call(s) lsp responses.
  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
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# Reference: built-in tools

> Neutral description of the tools declared to the model. A tool never returns a Go error for an ordinary failure: the failure text is returned to the model as the tool's output. Every tool records its call in the loop detector; the same tool, input and output three times in a row appends `[LOOP_DETECTED: …]` to the output.

> **Permissions.** "Permission requested" below applies when `yolo: false` (see [configuration](configuration.md)), in every front end: the line mode's `🔐` prompt, the window's box, the editor's dialog. With `yolo: true` (the default) the same calls run without asking. In `-p` and `mm --ask` a call that would ask is rejected. A refusal returns `[<tool> rejected by the user]` (`[command rejected by the user]` for `bash`) to the model.

## `bash`

Declared when `bashTool: true` (the default).

| Field | Type | Description |
|-------|------|-------------|
| `command` | string | The shell command to run. |

| Aspect | Behaviour |
|--------|-----------|
| Shell | `bash -c <command>`, a new shell per call, no stdin. |
| Working directory | The process directory in terminal mode; the session `cwd` in ACP mode. |
| Timeout | 30 s; the output then ends with `[error: 30s timeout exceeded]`. |
| Cancellation | The command belongs to the turn: cancelling the turn (`Esc`, `Ctrl-C`, `/abort`, or asking something else) kills it, and the output ends with `[error: cancelled]`. |
| Background jobs | The output is collected through a temporary file, not a pipe, so the tool returns as soon as the shell exits even when the command left a job running (`nohup serve &`). The job keeps running — that is usually the point — and writes into a file nobody reads. Through a pipe, `os/exec` waits for *every* process holding the write end, so such a command blocked the turn for ever. |
| Very large output | Read back from both ends (1 MiB each) before `maxOutput` truncation, with `[... output too large to read back in full ...]` between them. |
| Output | stdout and stderr merged, truncated to `maxOutput` characters. |
| Non-zero exit | Output ends with `[exit code: exit status N]`. |
| Terminal display | `🛠️  bash: <command>` before running (then the `🔐` prompt when permission is requested), then up to `previewLines` lines dimmed and indented, or `(no output)`. |
| ACP | Tool call of kind `execute`; permission requested before running; a refusal returns `[command rejected by the user]` with status `failed`. |
| Window mode | One `‣ bash  <command>` line with status, time and folded output. Permission requested: waits for `a`/`s`/`r`/`n`; a refusal returns the same text as in ACP. |

## `read_skill`

Declared only when `skillsDir` holds at least one skill (`<name>.md` or `<name>/SKILL.md`).

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Skill name, exactly as listed in the tool description. |

| Aspect | Behaviour |
|--------|-----------|
| Description | Fixed sentence followed by the catalogue: one `  <name> — <description>` line per skill, sorted by name. |
| Output | The full markdown of `<skillsDir>/<name>.md`, or of `<skillsDir>/<name>/SKILL.md` when there is no flat file, truncated to `maxOutput`. |
| Unknown name | `No skill named "x". Available skills: a, b, c` |
| Path safety | Only the base name of `name` is used; no directory traversal. |
| Terminal display | `📖 read_skill: <name>`; the content is not echoed. |
| ACP | Tool call of kind `read`; no permission dialog. |

## `read_file`

Declared when `editTools: true`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `path` | string | — | File path, relative to the working directory. |
| `start` | int | `0` | First line (1-based); `0` = from the start. |
| `end` | int | `0` | Last line (1-based, inclusive); `0` = to the end. |
| `numbered` | bool | `false` | Prefix each line with its number. |

| Aspect | Behaviour |
|--------|-----------|
| Missing file | `<path> does not exist. Create it with write_file` |
| Output | The lines, truncated to `maxOutput`; the final newline is kept only when the end of the file is shown. |
| ACP | Kind `read`, with the absolute path as location. |

## `write_file`

Declared when `editTools: true`.

| Field | Type | Description |
|-------|------|-------------|
| `path` | string | File path, relative to the working directory; parent directories are created. |
| `content` | string | The whole content of the file. |

| Aspect | Behaviour |
|--------|-----------|
| Existing file | Entirely replaced. |
| Identical content | Nothing is written; the result says so. |
| Output | A one-line headline and the diff against the previous content. |
| ACP | Kind `edit`; permission requested; the change travels as a diff (new-file diff when created). |

## `edit_file`

Declared when `editTools: true`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `path` | string | — | Path of an existing file. |
| `edits` | array of `{old, new}` | — | Replacements, all resolved against the original file. |
| `edits[].old` | string | — | Exact text to find, present exactly once. Whitespace is forgiven — see *How `old` is matched*. |
| `edits[].new` | string | — | Replacement; empty deletes the matched text. |
| `dry_run` | bool | `false` | Report what would change, write nothing. |

| Rule | Consequence when broken |
|------|-------------------------|
| `old` appears exactly once | Refused: not found, or ambiguous — `old text appears N times (lines 11, 20) — to change ONE of them, include its surrounding lines in old until the text is unique; to change several, send one edit per place…`. |
| Two edits must not overlap, nest or repeat | Refused. |
| Edits are resolved against the original text, never in cascade | — |
| Any refused edit | Nothing is written. |

| Aspect | Behaviour |
|--------|-----------|
| Output | A one-line headline (edits applied, lines added and deleted, first changed line, and ` · N matched ignoring whitespace` when any edit needed a relaxed match) and the diff with line numbers. |
| ACP | Kind `edit`; permission requested; the change travels as a diff with old and new text. |

### How `old` is matched

Three passes, strictest first; **the first pass that finds anything at all wins**, so a file that contains the text verbatim is never matched any other way.

| Pass | What it forgives | Reported as |
|---|---|---|
| 1. Exact | Nothing. `old` is searched byte for byte. | — |
| 2. Edges | The whitespace *around* `old`: an indent the model kept on a single line, a leading or trailing newline. The same padding comes off `new` when `new` carries it, so a `new` that deliberately adds a blank line still adds it. | `matched ignoring whitespace` |
| 3. Indent | Every line's leading and trailing whitespace, for an `old` spanning two lines or more. This is the tabs-versus-spaces case: a model retypes a Go block with four spaces where the file has a tab. | `matched ignoring whitespace` |

Two things are **not** relaxed:

- **Uniqueness.** A loose match must still be the only one. Two of them are refused exactly as two exact ones are, with the same message and the same line numbers — that rule is what makes an edit verifiable.
- **The file's indentation.** Before writing, `new` is re-indented to the file's own base indent and unit, so a tab-indented file does not end up with a block of spaces in the middle of it. A blank line stays empty rather than becoming a line of spaces.

The headline then ends with ` · N matched ignoring whitespace`. It is reported rather than hidden: a model told its text did not match exactly writes the next one better.

When nothing matches, the refusal shows the region of the file that came closest, with real line numbers — half the lines have to line up before an excerpt is offered, since pointing at an innocent part of the file is worse than saying nothing:

```
main.go: edit #1: old text not found — it must match the file exactly, including indentation and line breaks:
  │ func main() {
  │     count := 2
  │     print(count)
  │ }
the closest the file comes to it — copy these lines:
  │ 3 │ func main() {
  │ 4 │ 	count := 1
  │ 5 │ 	print(count)
  │ 6 │ }
```

## `lsp`

Declared when `lsp.servers` lists at least one server whose command is on the `PATH` at start-up. A listed server that is not found is skipped with a `[warning: lsp server <name> (<command>) not found on the PATH — skipped]`; when none is found the tool is not declared and the banner says so.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `action` | string | — | `definition`, `references`, `hover`, `symbols`, `diagnostics` or `rename`. |
| `path` | string | — | File path, relative to the working directory. Its extension picks the server. |
| `symbol` | string | `""` | The identifier, as written in the code. Stray quotes and punctuation are cut off and a `receiver.` or `pkg.` prefix dropped (`Greet', ` and `person.Greet` both mean `Greet`). Its occurrences in the file are tried in order — code lines first, comment lines last — until the server answers. For `definition`, `references`, `hover`, `rename`. |
| `line` | int | `0` | 1-based line. With `symbol`: restricts the search to that line. With `column`: the exact position. |
| `column` | int | `0` | 1-based column, in characters, pointing inside the identifier. Only needed without `symbol`. |
| `new_name` | string | `""` | The new identifier, for `rename`. Cleaned like `symbol`. When it is empty and `symbol` holds several words (`Greet 1 12 Hello` — a small model writing symbol, line, column and new name into one string), the first identifier is the symbol and the last the new name. |

| Aspect | Behaviour |
|--------|-----------|
| Description | Names the servers and their extensions (`Servers available: gopls (.go), …`) and the six actions. |
| `definition`, `references` | One `path:line:column  <source line>` per location, sorted by path then position, duplicates removed; paths inside the project are relative, others absolute. `references` is prefixed with `N reference(s):` and includes the declaration. |
| `hover` | The server's text as it sent it (signature, type, documentation; Markdown for most servers). |
| `symbols` | One `<kind> <name>  [<detail>]  line N` per symbol, children indented two spaces. |
| `diagnostics` | The file is synced, then the call waits up to 3 s for the server's publication. `N diagnostic(s) in <path>:` then one `<severity> path:line:column <message> [<source>]` per finding; `no diagnostics: <path> is clean`; ` (as last published)` when the file did not change since the server last spoke; `no diagnostics received for <path> within 3s (…)` when it never did. |
| `rename` | Applies the server's edits to the files on disk (UTF-16 positions converted), tells the server the new contents, and answers `renamed to <new>: N edit(s) in M file(s)` followed by one `path: N edit(s)` line per file. Without `new_name`: `[lsp: rename needs new_name — call lsp again with {"action": "rename", "path": …, "symbol": …, "new_name": "<the new identifier>"}; do not use edit_file for a rename]`. |
| Positions | 1-based lines and columns at the tool's boundary, 0-based UTF-16 on the wire. A position past the end of a line or of the file is refused with the count. |
| File sync | Before every request the file is read from disk and sent as `didOpen` (first time) or a whole-document `didChange` (when its content moved): edits made with `edit_file` are seen without any extra step. |
| Server lifecycle | Started on the first call for one of its extensions, in the project root (the directory `mm` runs in), kept for the session, stopped (`shutdown`, `exit`, then the process) when `mm` exits. A server that died is started again on the next call. |
| Failures | Always text, never a Go error: `[lsp: no language server for "x.md" — configured: gopls (.go)]`, `[lsp: "foo" not found in main.go]`, `[lsp: column 40 is past the end of line 3 (15 characters)]`, `[lsp: gopls: hover failed: …]`, `[lsp: no definition found for the symbol at main.go:5:4]`. |
| Terminal display | `🔎 lsp: definition main.go greet` (or `…:9:14` with a position, `rename main.go greet → hello`), then the answer echoed like a command's output. |
| ACP / window | Kind `read` for navigation, no permission; `rename` is kind `edit` and asks for permission like `edit_file` (`[rename rejected by the user]` when refused). |
| Counter | `🔎 N lsp call(s)` in the recap line (see below). |

## `fetch_content`

Declared only with `fetchContentTool: true`. Downloads a web page and returns it as Markdown, or saves the Markdown to a file. Package `internal/webmd`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `url` | string | — | The page. A missing scheme means `https://` (`k33g.org`); a scheme typed without its colon is repaired (`https//k33g.org`). Only `http` and `https`. |
| `output` | string | `""` | File to save the Markdown to, relative to the working directory (the session `cwd` in ACP); parent directories are created. When set, the Markdown is not returned. |
| `baseUrl` | string | page URL | Absolute URL against which relative links and image sources are resolved. Default: the page's final URL, after redirections. |
| `tables` | string | `html` | Tables with `rowspan`/`colspan` or a nested table: `html` keeps them as raw HTML, `flatten` makes a GFM table and copies a spanned cell into every slot it covers. |
| `strikethrough` | bool | `false` | `<del>`, `<s>`, `<strike>` → `~~text~~`; otherwise the plain text. |
| `taskList` | bool | `false` | A list item starting with a checkbox → `- [ ] …` / `- [x] …`; otherwise the checkbox is dropped. |

| Aspect | Behaviour |
|--------|-----------|
| Request | `GET` with `User-Agent: mm/<version>`; at most 10 redirections; 30 s for the whole fetch; pages above 5 MiB refused. |
| robots.txt | Read (and cached 24 h per origin) before the page and before every redirection target, for the product token `mm`, else the `*` group. `Allow`/`Disallow`, longest match wins, `Allow` wins a tie, `*` and a final `$` supported (RFC 9309). A 4xx robots.txt allows everything; a 5xx or an unreachable one forbids everything. |
| Accepted types | `text/html`, `application/xhtml+xml`; no `Content-Type` → sniffed. |
| Encoding | In order: byte-order mark, `charset` of the `Content-Type` header, `<meta charset>` or `<meta http-equiv="Content-Type">` in the first 4 KiB, UTF-8 if the bytes are valid UTF-8, else `windows-1252`. An unknown name is skipped. The content is converted to UTF-8. |
| Removed | `script`, `style`, `noscript`, `template`, `meta`, `link`, `base`, `title`, `svg`, `canvas`, `iframe`, `object`, `embed`, `nav`, `aside`, `dialog`, `button`, `select`, `textarea`, inputs other than checkboxes, a `footer` outside `article`/`main`, HTML comments, ARIA roles `navigation` `banner` `complementary` `search` `contentinfo` `dialog`, `aria-hidden="true"`, `hidden`, class/id words `ad` `ads` `advert` `sponsor` `promo` `cookie` `consent` `gdpr` `newsletter` `subscribe` `popup` `modal` `share` `social` `breadcrumb` `nav` `navbar` `navigation` `menu` `sidebar` `skiplink` (and a few variants), classes `sr-only` `visually-hidden` `screen-reader-text`. An element matched by class/id is kept when it contains `main`, `article` or `h1`. |
| Attributes | All removed except `a[href]`, `img[src,alt]`, `td/th[colspan,rowspan]`, `ol[start]`, `input[type,checked]`, and the `language-*`/`lang-*` words of a `pre`/`code` class. |
| Conversion | `h1`–`h6` → `#`–`######`; `p` → paragraph, blank line between blocks; `strong`/`b` → `**…**`; `em`/`i` → `*…*`; inline `code`/`kbd`/`samp`/`tt` → backtick span; `pre` → fenced block (info string from `language-x`), fence longer than any backtick run inside; `br` → hard break `\`; `hr` → `---`; `blockquote` → `> `; `a` → `[text](url)`, the URL when there is no text, text only for `javascript:`; `img` → `![alt](src)`, `![](src)` without alt, alt only for a `data:` URI; `ul`/`ol` → `- ` / `N. ` (from `start`), nested lists indented by the parent marker's width; simple tables → GFM, first row as header, `|` escaped, `<br>` for line breaks in a cell, `<caption>` as a paragraph above. |
| Unknown element | Tags dropped, text kept; one that contains a block element is treated as a container. |
| Empty element | Nothing, no blank line. |
| Invalid HTML | Repaired by the HTML5 parser (`golang.org/x/net/html`), then converted. |
| Escaping | Backslash, `*`, backtick, `[`, `]`, `<`, and a `_` not inside a word get a backslash; so does a line starting with `#`, `>`, `-`, `+`, `=`, `\|` or `N.`. |
| Result | The Markdown, ending with exactly one newline; no title, front matter or preamble. Truncated to `maxOutput` characters. |
| With `output` | `[fetch_content: saved N lines of Markdown (B bytes) from <url> to <file> — the content is not shown]`. |
| Page with no content left | `[fetch_content: <url> has no text content once scripts, navigation and ads are removed — nothing saved]`; no file written. |
| Failure | `[fetch_content: error N (<class>) — <reason>]`, see the codes below. |
| Terminal display | `🌐 fetch_content: <url>` (`→ <file>` with `output`), spinner `Fetching`, then the result echoed like a command's output (only the confirmation line with `output`). |
| ACP / window | Kind `fetch`. No permission without `output`; with `output`, permission requested like `write_file` (`[fetch_content rejected by the user]` when refused), and the file's absolute path as location. |

| Code | Class | Cause |
|------|-------|-------|
| `1` | `conversion` | Not an HTML page, larger than 5 MiB, undecodable, or the output file cannot be written. |
| `2` | `input` | Host unreachable, timeout, HTTP status ≥ 300 after redirections, more than 10 redirections, robots.txt forbids the page or cannot be read. |
| `3` | `usage` | Empty or unparsable `url`, a scheme other than `http`/`https`, unknown `tables` value, `baseUrl` not absolute. |

## `ask_agent`

Declared when `mmConfigs` names at least one agent. Hands a task to one of them and returns its answer as the tool's result: the model decides when, from the agents' descriptions. Same machinery as `/ask` (package `internal/ask`): the agent runs as a child `mm -acp`, started on its first call and reused, with a fresh session per call.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `agent` | string | — | A name of `mmConfigs`. |
| `prompt` | string | — | The task, complete and self-contained. |
| `with_context` | bool | `false` | Send this conversation before the prompt: the history the turn started from (questions, answers, one line per tool call, never tool outputs), capped at `maxOutput` characters, newest kept. |

| Aspect | Behaviour |
|--------|-----------|
| Description | A fixed paragraph, then `Agents:` and one `  <name> — <purpose>` line per agent, sorted by name. `<purpose>` is the entry's `description`, else the agent file's `acp.title` (unless it is the built-in one), else `(no description)`. |
| Permission | The call itself is gated like `bash`: requested when `yolo: false`. |
| The asked agent's calls | Decided by its entry's `yolo` in `mmConfigs` when set (`true` run, `false` asked), else by this agent's `yolo`. |
| Working directory | The process directory in the terminal and the window; the session `cwd` in ACP. |
| Display | The asked agent's text, thoughts and tool calls stream through the turn, its tool calls titled `<agent> · <title>`. The window names it while it works: header `mm [<acp.title>] · <model> @ <provider> · asked by <this agent>`, its answer under `‣ mm [<acp.title>]` (its `mmConfigs` name when it has no title). Line mode: `🗣 ask_agent: <agent> (<provider>/<model>) — <first line of the prompt>`, then the agent's output. |
| Watchdog | Each event of the asked agent (text, thought, tool call, answered permission) counts as a sign of life for this agent's watchdog. Silence longer than `watchdogTimeout` — a model loading, a long prompt being read — still ends the turn. |
| Result | The answer, truncated to `maxOutput`. The tool call's own output in the window and the editor is `<agent> answered (N characters)`: the answer has already streamed. Not added to the history as a `/ask` exchange: it is a tool result. |
| Errors | `[ask_agent: unknown agent "x" — agents: a, b]`; `[ask_agent: the prompt is empty — say what <agent> must do]`; `[ask_agent: <agent> failed: <reason>]`; `[ask_agent: <agent> gave no answer]`; a cancelled or broken run returns what was answered, then `[ask_agent: <agent> stopped before finishing: <reason>]`. |
| Depth | An asked agent may itself have `mmConfigs` and call `ask_agent`, up to two levels (`MM_ASK_DEPTH`); beyond, the call fails with the depth message. |

## Loop detection

Every tool records `(tool, input, output)` in the loop detector after each call.

| Situation | Behaviour |
|-----------|-----------|
| The same triple 3 times in a row | The tool's output gets `[LOOP_DETECTED: You have attempted the same action multiple times with the same result. Please try a different approach or provide more information.]` appended. Nothing stops. |
| 3 more identical calls after that (6 in a row) | The tool returns an error instead of text and the turn ends: `[error: the model repeated the same edit_file call 6 times in a row with the same failing result — turn stopped by the loop detector; rephrase the request, or fix what the tool kept reporting]`. The history keeps everything that ran; the error is recorded in the session file's `errors` array (`where: generation`). |
| A different call | The count starts again. `/new` resets the detector; `/compact` does not. |
| A tool call whose streamed arguments repeat a pattern (the last 256 characters are one 4–64-character pattern repeated) or exceed 256 KB | The generation is stopped at once: `[error: the model's tool call ran away: its arguments repeat the same pattern (N characters so far: …) — generation stopped; rephrase the request, or try another model]`. Without it a derailed model composes garbage for minutes while the screen shows "Thinking" (the watchdog is fed by every delta). While a tool call streams, the spinner says `Composing a tool call`. |

The thresholds (3, then 3 more) are set in `main.go`; `maxTurns` remains the outer bound of a turn.

## Counters shown after a turn

| Counter | Counts |
|---------|--------|
| `⚙ N command(s)` | `bash` tool responses in the turn. |
| `📝 N file op(s)` | `read_file`, `write_file`, `edit_file` responses. |
| `📖 N skill(s)` | `read_skill` responses. |
| `🔎 N lsp call(s)` | `lsp` responses. |