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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
|
# 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` → ``, `` 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. |
| Hooks | The asking agent's `preAsk` hooks run after the permission, before the agent is asked; its `postAsk` hooks run on the answer before the model reads it. A denied hand-off returns `[ask denied by a hook: <reason>]`; a replaced answer makes the call's output `<agent> answered; a hook replaced the answer (N characters)`; hook context follows the answer after `[context added by a hook]`. See [hooks](hooks.md#preask). |
| 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. |
## `ask_user`
Declared unless `askUserTool: false` (on by default), and never beside a `structuredOutput`. Puts one to four questions to the user and returns the answers. Not gated by `yolo`: asking changes nothing.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `questions` | array of objects | — | 1 to 4 questions, asked together. A single question sent without the list (its fields at the top level) is accepted. |
| `questions[].question` | string | — | The question. |
| `questions[].type` | string | `choice` when `options` are given, `text` otherwise | `choice` (one option), `multi` (any number of options), `text` (a free answer), `confirm` (yes or no). Synonyms accepted: `single`, `select`, `radio`, `enum`, `one` → `choice`; `multiple`, `multiselect`, `checkbox`, `checkboxes`, `many` → `multi`; `input`, `string`, `free`, `freetext` → `text`; `yesno`, `yes_no`, `boolean`, `bool`, `confirmation` → `confirm`. |
| `questions[].options` | array of strings | — | For `choice` and `multi`: at least two, trimmed, duplicates (ignoring case) dropped. Ignored for `text` and `confirm`. |
| `questions[].default` | string | none | Kept only when it means something: one of the options (`choice`), a yes or a no in English or French (`confirm`), any text (`text`). None for `multi`. |
The declared schema describes a question's fields in words, without `properties`: llama.cpp would impose them in alphabetical order (see [the explanation](../explanation/asking-the-user.md)).
| Aspect | Behaviour |
|--------|-----------|
| Line mode | `❓ ask_user: N question(s)`, then each question in turn: `❓ <question>`, one ` N. <option>` line per option (`(default)` after the default), a hint, and ` ? `. One typed line answers it. `choice`: a number, an option's text, the start of exactly one option, or any other text (taken as the answer). `multi`: numbers separated by commas, spaces or semicolons, all in range. `confirm`: `y`/`yes`/`o`/`oui`/`true`/`1` or `n`/`no`/`non`/`false`/`0`. `text`: the text. An empty line takes the default, else asks again; a line that does not answer prints the hint again. `/skip` declines; other slash commands keep their meaning (`/abort`, `/quit` stop the turn and cancel the question). |
| Window (`-tui`) | The question replaces the input box, `(i/N)` after it when there are several; the options are drawn over the bottom of the conversation, the highlighted one in reverse with `❯`. `choice`: ↑↓ (wrapping) and Enter, or a digit 1–9. `multi`: ↑↓, Space ticks (`[x]`), Enter sends when at least one is ticked. `confirm`: `y`/`o` or `n`, or ←→↑↓ and Enter. `text`: typed in the box (prefilled with the default), Backspace, Enter. Escape declines the whole form. The tool call shows `… waiting for your answer`. |
| Editor (ACP) | The client advertised `clientCapabilities.elicitation.form`: one `elicitation/create` request per call (see [ACP](acp.md#elicitationcreate-requests-sent)). Otherwise, when every question is a `choice` or a `confirm`: one `session/request_permission` dialog per question, its buttons the options (`Yes`/`No` for a `confirm`) and `Decline`. Otherwise: nobody can be asked. |
| Asked agent (`/ask`, `ask_agent`) | Its questions come up to the asking agent's user, through the asking agent's own front end, each prefixed `<agent> asks: `. With `mm --ask`, nobody can be asked. |
| One-shot (`-p`) | Nobody can be asked. |
| Watchdog | While a question waits, the generation's watchdog is beaten every second: waiting for a human does not end the turn. A stopped turn cancels the question. |
| Result | Answered: `The user answered:` then one `- <question> → <answer>` line per question (several options joined by `, `). Declined: `[the user declined to answer: do not ask the same thing again; go on without it or stop]`. Cancelled: `[the question was cancelled before the user answered]`. Nobody to ask: `[the user cannot be asked in this mode: make a reasonable assumption, say which, and go on]`. Anything but an answer ends the call as failed in the window and the editor. |
| Errors | `[ask_user: no question — send questions: [{question, type, options}]]`; `[ask_user: N questions — ask at most 4 at a time]`; `[ask_user: question N has no text]`; `[ask_user: question N is a choice question — give it at least two options]` (or `multi`); `[ask_user: question N has type "x" — use choice, multi, text or confirm]`. |
## `save_answer`
Declared when `saveAnswerTool: true` (default `false`).
| Field | Type | Description |
|-------|------|-------------|
| `path` | string | The file to create, relative to the working directory. It must not exist yet. |
| Aspect | Behaviour |
|--------|-----------|
| Text written | The text of the model message that made the call, when it has any; otherwise the last answer of the conversation as it stood when the turn started. Written as it is, with a trailing newline; the model does not generate it again. |
| Existing file | Never overwritten: `[save_answer: <path> already exists — nothing was written. Pick another name, or ask the user]`. |
| No answer yet | `[save_answer: there is no answer to save yet — write the answer in your reply first, then call save_answer with the file name]`; nothing is asked. |
| Directories | Missing parent directories are created. |
| Result | `saved <this answer\|the previous answer> to <path> (N bytes), starting with: <first line>`. |
| Working directory | The process directory in terminal mode; the session `cwd` in ACP mode and the window. |
| Permission | Requested when `yolo: false`, as for `write_file`; a refusal returns `[save_answer rejected by the user]`. |
| Terminal display | `💾 save_answer: <path> (this answer\|the previous answer)`, then the result. |
| ACP and window | Tool call of kind `edit`, with the file's absolute path. |
## `unload_model`
Declared when `unloadModels` is true (the default), except beside a `structuredOutput`.
| Field | Type | Description |
|-------|------|-------------|
| `model` | string | The model to unload, as the server names it. Empty: the agent's own model. |
| Aspect | Behaviour |
|--------|-----------|
| llmman, `ollama` | Looks the model up in `GET /api/ps` first; when it is loaded, `POST /api/generate {"model": …, "keep_alive": 0}` and expects `done_reason: "unload"`. A model that is not loaded is never asked for: the request would load it first. |
| `llamacpp` | `POST /models/unload {"model": …}`, which exists in router mode only (llama-server started without a model). A single-model server's 404 gives the reason and the alternatives: router mode, `--sleep-idle-seconds N`, stopping the server. |
| `dmr` | `cannot unload <model>: the dmr provider cannot unload a model`. |
| Result | One line: `unloaded <model>`, `<model> was not loaded: nothing to unload`, or `cannot unload <model>: <reason>` (unreachable server, the server's own error, no confirmation). |
| Own model | Unloading the agent's own model lasts until its next step: the model is needed to read the result, and the server reloads it. |
| Permission | Never asked, even with `yolo: false`: nothing changes on disk, and the model reloads by itself. |
| Terminal display | `🧹 unload_model: <model>` (`(own model)` when empty), then the result. |
| ACP and window | Tool call of kind `other`; a `cannot unload` result is a failed call. |
| Timeout | 30 s per request to the server. |
## `search_memory`
Declared when `sessions.enabled` and `sessions.autoSearchMemory` are both true (both default to false). The same search, for the user, is the `/memory <words>` command (`sessions.searchMemory`, see [slash commands](slash-commands.md)).
| Field | Type | Description |
|-------|------|-------------|
| `query` | string | The words to look for. |
| `limit` | integer | How many hits at most. `0` or absent: 10; capped at 50. |
| Aspect | Behaviour |
|--------|-----------|
| Where | The session files under `sessions.dir`, resolved against the working directory: the process directory in terminal mode, the session `cwd` in ACP mode and the window. |
| Matching | The query is split into words; case and accents do not count (`preference` finds `Préférence`); a word matches anywhere in a text, also inside a longer word. Words of one or two letters (`de`, `a`, `of`) are left out when the query has longer ones. A message is a hit when it holds at least one word. |
| Searched | User questions (without the context a `preGeneration` hook appended), the model's answers, the notes of `/compact` summaries (without their wrapper line). A question that matches carries the answer its turn ended on, which is then not a hit of its own; its score counts the words of both. Not searched: the acknowledgement after a summary, tool calls and outputs, mm's own notes (`[mm: …`, a blocked prompt's marker), the `errors` array. |
| Left out | Every message of the conversation in progress, wherever it is stored (same role and text). |
| Order | By number of distinct words held, then by the session's last update (newest first), then by turn (latest first). |
| Result | `🔎 N hit(s) for "<query>" in <dir>`, then per hit `<session id> · <date> · turn <n> · question\|answer\|summary` and the text from the first word found to the last (80 characters before, at least 160 after the first, 600 at most, on one line, `…` where cut); for a question, a third line `→ <its answer>` — the text around the words, or its beginning. No hit: `🔎 no hit for "<query>" in <dir>` followed by `The search matches words, not meanings: try again with other words — synonyms, or the words in the language the earlier conversation was likely held in (often English).` An empty query: `⚠️ nothing to search: give one or more words`. Capped by `maxOutput`. |
| Permission | Never asked, even with `yolo: false`: the tool only reads. |
| Terminal display | `🔎 search_memory: "<query>"`, then the result. |
| ACP and window | Tool call of kind `search`. |
## Declared tools
Declared from the `tools:` block of the agent file, one tool per entry (see [configuration](configuration.md#tools-keys)). Each has the entry's `name`; its fields are the entry's `parameters`.
| Aspect | Behaviour |
|--------|-----------|
| Schema sent to the model | One property per parameter with its `type` and a description made of the parameter's `description`, `One of: …` for an `enum`, and `Optional.` or `Optional, default <v>.` for an optional parameter. No `required` and no `enum` keywords. The tool's description ends with `Required parameters: <names>. Give each parameter as its own field.` |
| Values | Coerced to the parameter's type. `number` and `integer` also accept a numeric string (`"3"`), `boolean` also accepts `"true"`/`"false"` (any case), `string` also accepts a number or a boolean. Rendered as text: integers without a decimal point, numbers without an exponent, booleans as `true`/`false`. |
| Optional parameter left out | Its `default` when there is one. Otherwise a command entry that is only its placeholder (`"{{p}}"`) is removed; an entry that contains it with other text gets an empty string in its place. |
| Invalid call | Nothing runs and nothing is asked. The output is `[<name>: invalid call, nothing was run — <faults>. You sent: <the call's input as JSON>. Expected: <each parameter with its type and description>. Call <name> again with each parameter as its own field.]`. Faults, all listed: `<p> is missing`, `<p>: "<v>" is not an integer` (or a number, a boolean, a string), `<p>: <v> is not one of <values>`, `"<key>" is not a parameter of this tool`. |
| Execution | The command's argv, without a shell: a value never adds an argument, a pipe or a redirection. |
| Working directory | The process directory in terminal mode; the session `cwd` in ACP mode and the window. |
| Timeout | The entry's `timeout`, else `bashTimeout`; the output then ends with `[error: <timeout> timeout exceeded]`. |
| Program not found | `[error: <program> cannot be started: <reason>]`. |
| Cancellation, output, non-zero exit | As `bash`. |
| Permission | Requested when `yolo: false`; a refusal returns `[<name> rejected by the user]`. |
| Terminal display | `🧰 <name>: <argv>` (or `🧰 <name>: <input>` for an invalid call), then the output like `bash`'s, the refusal text included. |
| ACP and window | Tool call of kind `execute`, title `<name>: <argv>`. |
| Recap | Not counted in `⚙ N command(s)`. |
| Hooks | `preToolCall` and `postToolCall` apply, with the tool's name as `matcher`. |
## 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. |
|