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
|
# Reference: configuration file
> Neutral, exhaustive description of the YAML file read by `mm`. Every key is optional; a key left out keeps its built-in default. The file is decoded on top of the defaults, so unknown keys are ignored.
## Top-level keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `provider` | string | `dmr` | Registry key of the LLM server: `dmr`, `llamacpp`, `llmman` or `ollama`. Overridden by `AGENT_PROVIDER`. |
| `model` | string | `huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M` | Model identifier in the provider's naming. Overridden by `AGENT_MODEL`. |
| `baseUrl` | string | `""` (provider default) | OpenAI-compatible endpoint tried first. Overridden by `AGENT_BASE_URL`. |
| `fallback` | string or absent | absent (provider default) | Endpoint used when `baseUrl` does not answer `/models` within 2 s. Absent = provider default; `""` = no fallback. |
| `apiKeyEnv` | string | `""` (provider default) | Name of the environment variable holding the API key. |
| `contextWindow` | int | `0` | Tokens the server serves. `0` = ask the server, else unknown. Must be ≥ 0. |
| `maxOutput` | int | `16000` | Maximum characters a tool returns to the model; longer output keeps its head and up to 2000 characters of tail. Must be > 0. |
| `maxTurns` | int | `10` | Maximum model ↔ tools round trips per question. Must be > 0. |
| `previewLines` | int | `20` | Lines of a command's output echoed to the terminal. `0` disables the echo. Must be ≥ 0. |
| `displayCommands` | bool | `false` | Print the numbered list of commands and file operations after each answer. |
| `showThinking` | bool | `true` | Show what a thinking model reasons about before it answers. Only the display is optional: the reasoning is always read off the wire, because it is what keeps the watchdog alive while the model thinks. See [Thinking models](../explanation/thinking-models.md). |
| `system` | string | built-in prompt | The system prompt, first message of every history. |
| `structuredOutput` | string (Go declarations) | `""` | The shape every answer must follow, as JSON: Go `type` declarations and one `var` naming the root of the answer. See [`structuredOutput`](#structuredoutput) below. Empty = free-text answers. Incompatible with any declared tool. |
| `skillsDir` | string | `skills` | Directory holding the skills, as `<name>.md` files or `<name>/SKILL.md` directories. A relative path is resolved from the directory `mm` is started in (terminal mode, or ACP mode without a config file), or next to the config file (ACP mode with one); the banner warns with the absolute path when nothing is found there. |
| `bashTool` | bool | `true` | Declare `bash`. `false` removes it: the model can still read and edit files (`editTools`) and load skills, but never runs a command. Reword a system prompt that still announces a bash tool. |
| `editTools` | bool | `true` | Declare `read_file`, `write_file`, `edit_file`. `false` leaves `bash` and `read_skill` only. |
| `fetchContentTool` | bool | `false` | Declare `fetch_content`, which fetches web pages as Markdown. Off by default: it is the only built-in tool that reaches the network. |
| `askUserTool` | bool | `true` | Declare `ask_user`, through which the model asks the user questions (pick one, pick several, type a text, yes or no) and waits for the answers. `false` removes it: the model cannot ask. Never declared beside a `structuredOutput`. Overridden by `AGENT_ASK_USER`; for an agent asked by another one, also by the asker's `mmConfigs.<name>.askUser`. See [tools](tools.md#ask_user). |
| `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. A key the OpenAI API does not declare is sent as it is, at the top level of the request — for example `chat_template_kwargs: {enable_thinking: false}`, which tells a llama.cpp server (`dmr`, `llamacpp`, `llmman`) not to reason; `reasoning_effort: none` does the same on Ollama. `messages`, `tools`, `tool_choice` (and the legacy `functions`, `function_call`) are ignored: Genkit builds them from the conversation. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). |
| `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. |
| `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama sends no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). |
| `bashTimeout` | duration | `30s` | How long one `bash` command may run before it is killed; the tool then answers `[error: <bashTimeout> timeout exceeded]`. Must be > 0. The watchdog keeps counting while a command runs (ticket 57): a command longer than `watchdogTimeout` ends the turn, so raise both together. |
| `context` | map | see below | Automatic history compression. |
| `sessions` | map | see below | Persistent, per-project sessions. |
| `trace` | map | see below | Write every request to the model and every response to files. |
| `agentsMd` | map | see below | Append the project's `AGENTS.md` to the system prompt of every conversation. On by default. |
| `acp` | map | see below | How `mm -acp` introduces itself to the editor. |
| `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. |
| `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. |
| `yolo` | bool | `true` | `true`: the tools run without asking. `false`: every call that changes something — `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename`, `plan` `create`/`update`/`next`/`delete` — waits for the user's answer in every front end (line mode, window, editor), and so does every MCP tool, whatever `mcp.permissions` says. Answers: allow once, allow this tool for the session, reject this call, never (this tool, for the session). `read_file`, `read_skill`, `fetch_content` without `output` and the `lsp` navigation actions never ask. In `-p` and `mm --ask`, nobody can answer: those calls are rejected. An agent started by `/ask`, `ask_agent` or `mm --ask` runs with `AGENT_YOLO=false` and submits its calls to the asking agent: the `yolo` of its `mmConfigs` entry decides when set, else the asking agent's `yolo`. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). |
| `permissions` | string | absent | Deprecated, replaced by `yolo`. `ask` = `yolo: false` (and wins over `yolo: true`); `auto` changes nothing. Either value adds a start-up warning. Must be `auto` or `ask`. |
| `mmConfigs` | map name → path, or name → `{path, description, yolo, askUser}` | none | Other mm agents this one can ask. Each name generates `/ask-<name>` and `/ask-with-context-<name>` (see [slash commands](slash-commands.md#asking-another-agent)), and the list declares the [`ask_agent`](tools.md#ask_agent) tool, through which the model calls them itself. The long form adds `description` (when to call that agent; shown in `ask_agent`'s description, falling back to the agent file's `acp.title`) and `yolo` (decides that agent's tool calls when it is asked, by `/ask`, `ask_agent` or `mm --ask`, instead of this agent's `yolo`: `true` they run, `false` each one is asked; absent, this agent's `yolo` decides) and `askUser` (whether that agent may ask the user questions when it is asked: `true` its `ask_user` questions come up to this agent's user, `false` its `ask_user` tool is not declared; absent, that agent's own `askUserTool` decides). `path` must not be empty. A relative path is relative to the directory of the file that declares it. Names: lowercase letters, digits and `-`, starting with a letter or a digit, not starting with `with-context`. A path whose file does not exist is a start-up warning, not an error. |
## `context` keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | bool | `false` | Turn the automatic compression on. `/compact` works regardless. |
| `threshold` | int | `75` | Percentage of the context window beyond which the history is compressed before the next question. Between 1 and 100. |
| `maxMessages` | int | `80` | Message count that triggers a compression when the window is unknown or the estimate is low. `0` disables. Must be ≥ 0. |
| `keepLastTurns` | int | `3` | Question turns kept raw at the end of the history. Must be ≥ 1. |
| `summaryMaxTokens` | int | `1200` | `max_tokens` of the summary request. Must be > 0. A thinking model's reasoning counts against it: on `dmr`, `llamacpp` and `llmman` the summary request turns reasoning off (`chat_template_kwargs.enable_thinking: false`), on `ollama` too (`reasoning_effort: "none"`, the one switch Ollama obeys) — whatever `sampling` says; other `chat_template_kwargs` are kept, and a `reasoning_effort` set there is lowered to `low` (gpt-oss has no "off"); elsewhere, if the error says the model "reasoned until the limit", raise it. |
| `prompt` | string | `""` | Replaces the built-in summary prompt when not empty. |
| `showStats` | bool | `true` | Print the one-line `🗜️` report after each compression. |
## `sessions` keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `sessions.enabled` | bool | `false` | Save every conversation to a file, rewritten after each turn, `/compact` and `/new`. The file also records, outside the history, every error shown (`errors` array: failed generation, failed compaction, failed `/resume`). Enables `/sessions` and `/resume` in the terminal and `session/load` over ACP. |
| `sessions.dir` | string | `.mm/sessions` | Directory of the session files, one `<id>.json` each. Relative to the directory `mm` is started from (terminal) or to the session's `cwd` (ACP); an absolute path is used as-is. Must not be empty. |
## `trace` keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `trace.enabled` | bool | `false` | Write one pair of files per model call: the request body as sent (after mm's own fields: `stream_options`, `return_progress`, a summary's reasoning switch), indented, and the response body as received. Overridden by `AGENT_TRACE`. Headers are never written. The banner ends with ` \| trace: <absolute dir>` while it is on (` \| trace: <session cwd>/<dir>` in ACP mode). |
| `trace.dir` | string | `.mm/traces` | Directory of the files. A relative path is resolved per call in the working directory of the turn — the session's `cwd` in ACP mode, like `sessions.dir` — else in the directory `mm` runs in; an absolute path is used as-is. A directory that cannot be created prints `[trace: cannot write to <dir>: <error> — nothing is traced]` once (on stderr in ACP mode); the calls go on. Created when needed. Must not be empty when the trace is on. |
File names: `<YYYYMMDD-HHMMSS.mmm>-<acp.name>-<NNNN>.request.json`, then `….response-<status>.sse` (a streamed answer, the server's SSE lines byte for byte, written as they arrive), `….response-<status>.json` (a whole JSON body, indented), or `….response-error.txt` (no response: the method, the URL and the error). `NNNN` counts the model calls of the process; a retry by the HTTP client is a call of its own. An agent started by `/ask` or `ask_agent` inherits `AGENT_TRACE` and writes beside its asker, under its own `acp.name`.
## `agentsMd` keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `agentsMd.enabled` | bool | `true` | Read the file at the start of every conversation: start-up, `/new`, `/resume`, a new ACP session (`session/new`), a reloaded one (`session/load`), `-p`. Never in the middle of a conversation. An agent started by `/ask`, `ask_agent` or `mm --ask` reads it only when its own file has it on; it looks in the working directory it is given (its asker's). |
| `agentsMd.path` | string | `AGENTS.md` | The file. A relative path is resolved against the conversation's working directory — the directory `mm` runs in, the session's `cwd` in ACP mode — not against the configuration file. An absolute path is used as-is. Must not be empty while `enabled` is `true`. |
| `agentsMd.maxChars` | int | `0` | Above this many characters (Unicode characters, after trimming) the file is not read at all, and the banner says so. Never truncated. `0` = no limit. Must be ≥ 0. |
When the file is read, the system message is the `system` prompt, a blank line, then:
```text
PROJECT INSTRUCTIONS (from <absolute path>)
The instructions of the project you work in. Follow them; when the user asks otherwise in the conversation, the user wins.
<the file, trimmed>
```
Not read: a missing file, a file of blanks only, a file above `maxChars`, any file when `structuredOutput` is set, a file that cannot be read (banner warning `<path> cannot be read: …`). A resumed session (`/resume`, `session/load`) continues under the file as it is now.
## `acp` keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `acp.name` | string | `bob` | `agentInfo.name` in the `initialize` response. Must not be empty. |
| `acp.title` | string | `Bob (bash-first agent)` | `agentInfo.title`, the name the editor may display. Empty: not sent. Also shown by the window mode (`-tui`) next to `mm`, in its header and above each answer — `‣ mm [Homer (coder)]` — unless it is empty or the built-in default. During an `/ask`, the window shows the ASKED agent instead: its `acp.title` (or its `mmConfigs` name) above its answer, and in the header with its model and `asked by <this agent>`, until it has answered. |
The version is not configurable: `agentInfo.version` is always the binary's, as printed by `mm --version`.
## `lsp` keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `lsp.servers` | list | `[]` | One entry per language server. The file extension picks the server. |
| `lsp.servers[].name` | string | — | How the banner, the tool description and the warnings call it. Required. |
| `lsp.servers[].command` | string | — | The executable, looked up on the `PATH` at start-up. Required. Not found: warning, server skipped. |
| `lsp.servers[].args` | list of strings | `[]` | Its arguments (many servers need `--stdio`). |
| `lsp.servers[].extensions` | list of strings | — | File extensions it serves, with the dot (`.go`, `.ts`). Required, at least one; matched case-insensitively; an extension may be served by one server only. |
```yaml
lsp:
servers:
- name: gopls
command: gopls
extensions: [".go"]
- name: typescript
command: typescript-language-server
args: ["--stdio"]
extensions: [".ts", ".tsx", ".js", ".jsx"]
```
The shipped `agent.lsp.yaml` is `agent.dmr.yaml` with this block on (gopls active, other servers ready to uncomment) and a system prompt that says when to use the tool.
## Provider defaults
| Provider | `baseUrl` | `fallback` | `apiKeyEnv` | Key required |
|----------|-----------|------------|-------------|--------------|
| `dmr` | `http://localhost:12434/engines/v1` | `http://host.docker.internal:12434/engines/v1` | none | no |
| `llamacpp` | `http://127.0.0.1:8080/v1` | none | `LLAMA_API_KEY` | no |
| `llmman` | `http://127.0.0.1:17434/v1` | `http://host.docker.internal:17434/v1` | `LLMMAN_API_KEY` | no |
| `ollama` | `http://localhost:11434/v1` | `http://host.docker.internal:11434/v1` | `OLLAMA_API_KEY` | no |
## `structuredOutput`
A YAML block string holding Go declarations. Each `type` is a definition; the one `var` (`var X T` or `var X = T{}`) names the root of the answer. Only `type` and `var` declarations are allowed.
| Go | JSON Schema |
|----|-------------|
| `string` | `{"type": "string"}` |
| `bool` | `{"type": "boolean"}` |
| `int`, `int8` … `int64`, `uint` … `uint64` | `{"type": "integer"}` |
| `float32`, `float64` | `{"type": "number"}` |
| `[]T` | `{"type": "array", "items": T}` |
| `map[string]T` | `{"type": "object", "additionalProperties": T}` |
| `struct { … }` | `{"type": "object", "properties": …, "required": [every key], "additionalProperties": false}`, keys in declaration order |
| a declared type `Name` | `{"$ref": "#/$defs/Name"}`; a struct root is inlined, `$defs` holds the types the root reaches |
| `*T` field, or `json:",omitempty"` | the key stays required, its value is `anyOf: [T, null]` |
| Field element | Effect |
|---------------|--------|
| `json:"name"` tag | the key; without a tag, the Go field name as written |
| `json:"-"` tag | field skipped |
| `//` comment above or beside a declaration or field | its `description` |
Refused, with `structuredOutput:<line>:<col>: …` counted from the first line of the block: `func`, `import`, `const`; generic types; aliases (`type A = B`); embedded fields; fixed-size arrays; map keys other than `string`; `interface`, `chan`, `func`, qualified (`time.Time`) and undefined types; a pointer outside a struct field; a type declared twice; a key used twice; zero or more than one `var`; a `var` without a type.
When the block is not empty:
- the declarations are appended to `system`, under an `OUTPUT FORMAT` paragraph;
- every model call carries `response_format: {"type": "json_schema", "json_schema": {"name": "output", "strict": true, "schema": …}}`;
- the answer is validated against the schema; one that does not match fails the turn with `model failed to generate output matching expected schema: …`, prefixed with `the answer was cut off by max_tokens before its JSON was complete …` when the server stopped on `max_tokens`;
- `-p` prints the JSON answer alone on stdout (see [Prompt mode specifics](cli.md#prompt-mode-specifics));
- the banner shows `tools: none (structured output: JSON answers)` and no "no skills" / "no tool" warning.
Example:
```yaml
structuredOutput: |
type Language struct {
Name string `json:"name"` // the language's name, in English
Official bool `json:"official"`
}
type Country struct {
Name string `json:"name"`
Capital *string `json:"capital"` // null when unknown
Languages []Language `json:"languages"` // at most 4
}
var Countries = []Country{}
```
## Validation errors
Reported on stderr as `[config error: <path>: <message>]`, exit code 1 — in every mode, so that `-p … | jq` never reads an error as the answer. `[engine error: …]` and `[usage: …]` too.
| Message | Cause |
|---------|-------|
| `provider must not be empty` | `provider: ""` |
| `model must not be empty` | `model: ""` |
| `contextWindow must be >= 0` | negative value |
| `maxOutput must be > 0` | zero or negative |
| `maxTurns must be > 0` | zero or negative |
| `previewLines must be >= 0` | negative value |
| `context.threshold must be between 1 and 100` | out of range |
| `context.keepLastTurns must be >= 1` | zero or negative |
| `context.summaryMaxTokens must be > 0` | zero or negative |
| `context.maxMessages must be >= 0` | negative value |
| `sessions.dir must not be empty` | empty string |
| `trace.dir must not be empty` | empty string with the trace on |
| `agentsMd.path must not be empty` | empty string with `agentsMd.enabled` on |
| `agentsMd.maxChars must be >= 0` | negative value |
| `acp.name must not be empty` | empty or blank string |
| `lsp.servers[N].name must not be empty` | a server without `name` |
| `lsp.servers[N].command must not be empty` | a server without `command` |
| `lsp.servers[N].extensions must list at least one extension` | a server without `extensions` |
| `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | an extension without its dot |
| `lsp.servers[N]: extension ".ts" is already served by "typescript"` | two servers on one extension |
| `permissions must be "auto" or "ask"` | any other value of the deprecated key |
| `AGENT_YOLO="maybe": must be true or false` | an `AGENT_YOLO` value `strconv.ParseBool` refuses (accepted: `1`, `t`, `true`, `0`, `f`, `false`, any case) |
| `bashTimeout must be > 0` | zero or negative |
| `structuredOutput:<line>:<col>: …` | a declaration the [`structuredOutput`](#structuredoutput) section cannot translate |
| `structuredOutput: no var declaration: …` | no `var` names the root |
| `structuredOutput cannot be used with tools — this agent declares: <tools> (…)` | a `structuredOutput` with at least one declared tool (`bash`, `plan`, the file tools, `fetch_content`, `read_skill`, `ask_agent`, `lsp`, MCP tools). Reported after the tools are assembled, before any front end starts. |
| `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` |
| `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |
| `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` |
| `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. |
A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`).
## Shipped files
| File | Purpose |
|------|---------|
| `agent.yaml` | Docker Model Runner set-up: `provider: dmr`, `maxTurns: 40`, `displayCommands: true`, compression off, a system prompt named "Bob" that describes the skills and file-editing rules. |
| `agent.llamacpp.yaml` | llama.cpp set-up: `provider: llamacpp`, `baseUrl: http://127.0.0.1:8080/v1`, `fallback: ""`, compression on. |
### Example
```yaml
provider: llamacpp
model: my-alias
baseUrl: http://127.0.0.1:8080/v1
fallback: ""
maxTurns: 40
context:
enabled: true
```
|