modified
docs/en/README.md +2 -1 | @@ -14,6 +14,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | ||
| 14 | 14 | - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demo-llmman/` |
| 15 | 15 | - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demo-ollama/` |
| 16 | 16 | - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>` |
| 17 | +- [See the JSON sent to the model and its answer](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, reading the files with jq | |
| 17 | 18 | - [Let your agent call other agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, `mmConfigs` descriptions, per-agent `yolo` |
| 18 | 19 | - [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context` |
| 19 | 20 | - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md) |
| @@ -61,7 +62,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | ||
| 61 | 62 | - [The `lsp` tool](explanation/lsp-tool.md) — one tool with an `action` field, a `symbol` field instead of columns, files synced from disk, rename gated by permission |
| 62 | 63 | - [The `fetch_content` tool](explanation/fetch-content-tool.md) — why a built-in tool beside MCP `fetch`, a home-made converter, robots.txt, errors as numbered text, known limits |
| 63 | 64 | - [Thinking models](explanation/thinking-models.md) — where a model's reasoning comes from, why `mm` reads it below Genkit, and why reading it keeps the watchdog alive |
| 64 | -- [Yolo mode and permissions](explanation/yolo-and-permissions.md) — why tools run unasked by default, existing versus asking, four answers in one package, why the asking agent decides | |
| 65 | +- [Yolo mode and permissions](explanation/yolo-and-permissions.md) — why tools run unasked by default, existing versus asking, four answers in one package, why the asking agent decides (and a per-agent `yolo` in `mmConfigs`) | |
| 65 | 66 | - [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, `ask_agent` and per-agent `yolo`, the depth limit |
| 66 | 67 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 67 | 68 | |
| @@ -14,6 +14,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | |||
| 14 | - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demo-llmman/` | 14 | - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demo-llmman/` |
| 15 | - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demo-ollama/` | 15 | - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demo-ollama/` |
| 16 | - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>` | 16 | - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>` |
| 17 | +- [See the JSON sent to the model and its answer](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, reading the files with jq | ||
| 17 | - [Let your agent call other agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, `mmConfigs` descriptions, per-agent `yolo` | 18 | - [Let your agent call other agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, `mmConfigs` descriptions, per-agent `yolo` |
| 18 | - [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context` | 19 | - [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context` |
| 19 | - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md) | 20 | - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md) |
| @@ -61,7 +62,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | |||
| 61 | - [The `lsp` tool](explanation/lsp-tool.md) — one tool with an `action` field, a `symbol` field instead of columns, files synced from disk, rename gated by permission | 62 | - [The `lsp` tool](explanation/lsp-tool.md) — one tool with an `action` field, a `symbol` field instead of columns, files synced from disk, rename gated by permission |
| 62 | - [The `fetch_content` tool](explanation/fetch-content-tool.md) — why a built-in tool beside MCP `fetch`, a home-made converter, robots.txt, errors as numbered text, known limits | 63 | - [The `fetch_content` tool](explanation/fetch-content-tool.md) — why a built-in tool beside MCP `fetch`, a home-made converter, robots.txt, errors as numbered text, known limits |
| 63 | - [Thinking models](explanation/thinking-models.md) — where a model's reasoning comes from, why `mm` reads it below Genkit, and why reading it keeps the watchdog alive | 64 | - [Thinking models](explanation/thinking-models.md) — where a model's reasoning comes from, why `mm` reads it below Genkit, and why reading it keeps the watchdog alive |
| 64 | -- [Yolo mode and permissions](explanation/yolo-and-permissions.md) — why tools run unasked by default, existing versus asking, four answers in one package, why the asking agent decides | 65 | +- [Yolo mode and permissions](explanation/yolo-and-permissions.md) — why tools run unasked by default, existing versus asking, four answers in one package, why the asking agent decides (and a per-agent `yolo` in `mmConfigs`) |
| 65 | - [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, `ask_agent` and per-agent `yolo`, the depth limit | 66 | - [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, `ask_agent` and per-agent `yolo`, the depth limit |
| 66 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) | 67 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 67 | 68 | ||
modified
docs/en/explanation/architecture.md +3 -3 | @@ -9,13 +9,13 @@ The entry point, `main.go`, only does wiring: it loads the settings, opens the e | ||
| 9 | 9 | | Package | Responsibility | |
| 10 | 10 | |---------|----------------| |
| 11 | 11 | | `config` | Every setting in one struct, with built-in defaults overridden by YAML and a few environment variables. | |
| 12 | -| `engine` | The connection to the model server and the generation itself: streaming, watchdog, non-streaming retry, summary requests, the provider registry, and the reasoning a thinking model streams beside its answer (see [Thinking models](thinking-models.md)). | | |
| 13 | -| `tools` | The built-in tools the model can call. | | |
| 12 | +| `engine` | The connection to the model server and the generation itself: streaming, watchdog, non-streaming retry, summary requests, the provider registry, the reasoning a thinking model streams beside its answer (see [Thinking models](thinking-models.md)), and the optional trace of every request and response (`trace.go`). | | |
| 13 | +| `tools` | The built-in tools the model can call, `ask_agent` included (it asks an `mmConfigs` agent through `ask`). | | |
| 14 | 14 | | `fileedit` | Exact-replacement file editing, the rules behind `edit_file`. | |
| 15 | 15 | | `lsp` | The language-server client behind the `lsp` tool: starts the configured servers on first use (config `lsp.servers`), syncs files from disk, asks for definition, references, hover, symbols, diagnostics and rename, and shapes the answers as `path:line:column <line>` text. Built on `go.lsp.dev/protocol` and `go.lsp.dev/jsonrpc2`. | |
| 16 | 16 | | `skills` | Discovery of markdown procedures and rendering of the `read_skill` catalogue. | |
| 17 | 17 | | `command` | The one table of slash commands: name, description, which front ends offer it, whether it takes an argument. The REPL dispatches on it, the ACP façade advertises its Editor slice, the window's `/` picker lists it. | |
| 18 | -| `ask` | `/ask-<name>`: starts another mm agent (config `mmConfigs`) as a child `mm -acp`, talks to it as an ACP client, streams its answer and tool calls through the asking front end's `ui.Sink`, and returns the exchange for the history. See [Asking another agent](asking-another-agent.md). | | |
| 18 | +| `ask` | `/ask-<name>`: starts another mm agent (config `mmConfigs`) as a child `mm -acp`, talks to it as an ACP client, streams its answer and tool calls through the asking front end's `ui.Sink`, and returns the exchange for the history; the same pool serves the `ask_agent` tool and `mm --ask`. See [Asking another agent](asking-another-agent.md). | | |
| 19 | 19 | | `clipboard` | Puts text on the system clipboard: a native tool on `PATH` first (`pbcopy`, `wl-copy`, `xclip`, `xsel`), OSC 52 as the fallback, and it reports which. Used by `/copy` in the REPL and the window. | |
| 20 | 20 | | `store` | Per-project session files (`<dir>/<id>.json`, whole rewrite, atomic): create, save, load, list, latest. Used by both front ends when `sessions.enabled`. | |
| 21 | 21 | | `mention` | The `@path` notation of a question: existing paths become `[attached file: …]` lines, the same shape as an editor's `resource_link`. Used by both front ends. | |
| @@ -9,13 +9,13 @@ The entry point, `main.go`, only does wiring: it loads the settings, opens the e | |||
| 9 | | Package | Responsibility | | 9 | | Package | Responsibility | |
| 10 | |---------|----------------| | 10 | |---------|----------------| |
| 11 | | `config` | Every setting in one struct, with built-in defaults overridden by YAML and a few environment variables. | | 11 | | `config` | Every setting in one struct, with built-in defaults overridden by YAML and a few environment variables. | |
| 12 | -| `engine` | The connection to the model server and the generation itself: streaming, watchdog, non-streaming retry, summary requests, the provider registry, and the reasoning a thinking model streams beside its answer (see [Thinking models](thinking-models.md)). | | 12 | +| `engine` | The connection to the model server and the generation itself: streaming, watchdog, non-streaming retry, summary requests, the provider registry, the reasoning a thinking model streams beside its answer (see [Thinking models](thinking-models.md)), and the optional trace of every request and response (`trace.go`). | |
| 13 | -| `tools` | The built-in tools the model can call. | | 13 | +| `tools` | The built-in tools the model can call, `ask_agent` included (it asks an `mmConfigs` agent through `ask`). | |
| 14 | | `fileedit` | Exact-replacement file editing, the rules behind `edit_file`. | | 14 | | `fileedit` | Exact-replacement file editing, the rules behind `edit_file`. | |
| 15 | | `lsp` | The language-server client behind the `lsp` tool: starts the configured servers on first use (config `lsp.servers`), syncs files from disk, asks for definition, references, hover, symbols, diagnostics and rename, and shapes the answers as `path:line:column <line>` text. Built on `go.lsp.dev/protocol` and `go.lsp.dev/jsonrpc2`. | | 15 | | `lsp` | The language-server client behind the `lsp` tool: starts the configured servers on first use (config `lsp.servers`), syncs files from disk, asks for definition, references, hover, symbols, diagnostics and rename, and shapes the answers as `path:line:column <line>` text. Built on `go.lsp.dev/protocol` and `go.lsp.dev/jsonrpc2`. | |
| 16 | | `skills` | Discovery of markdown procedures and rendering of the `read_skill` catalogue. | | 16 | | `skills` | Discovery of markdown procedures and rendering of the `read_skill` catalogue. | |
| 17 | | `command` | The one table of slash commands: name, description, which front ends offer it, whether it takes an argument. The REPL dispatches on it, the ACP façade advertises its Editor slice, the window's `/` picker lists it. | | 17 | | `command` | The one table of slash commands: name, description, which front ends offer it, whether it takes an argument. The REPL dispatches on it, the ACP façade advertises its Editor slice, the window's `/` picker lists it. | |
| 18 | -| `ask` | `/ask-<name>`: starts another mm agent (config `mmConfigs`) as a child `mm -acp`, talks to it as an ACP client, streams its answer and tool calls through the asking front end's `ui.Sink`, and returns the exchange for the history. See [Asking another agent](asking-another-agent.md). | | 18 | +| `ask` | `/ask-<name>`: starts another mm agent (config `mmConfigs`) as a child `mm -acp`, talks to it as an ACP client, streams its answer and tool calls through the asking front end's `ui.Sink`, and returns the exchange for the history; the same pool serves the `ask_agent` tool and `mm --ask`. See [Asking another agent](asking-another-agent.md). | |
| 19 | | `clipboard` | Puts text on the system clipboard: a native tool on `PATH` first (`pbcopy`, `wl-copy`, `xclip`, `xsel`), OSC 52 as the fallback, and it reports which. Used by `/copy` in the REPL and the window. | | 19 | | `clipboard` | Puts text on the system clipboard: a native tool on `PATH` first (`pbcopy`, `wl-copy`, `xclip`, `xsel`), OSC 52 as the fallback, and it reports which. Used by `/copy` in the REPL and the window. | |
| 20 | | `store` | Per-project session files (`<dir>/<id>.json`, whole rewrite, atomic): create, save, load, list, latest. Used by both front ends when `sessions.enabled`. | | 20 | | `store` | Per-project session files (`<dir>/<id>.json`, whole rewrite, atomic): create, save, load, list, latest. Used by both front ends when `sessions.enabled`. | |
| 21 | | `mention` | The `@path` notation of a question: existing paths become `[attached file: …]` lines, the same shape as an editor's `resource_link`. Used by both front ends. | | 21 | | `mention` | The `@path` notation of a question: existing paths become `[attached file: …]` lines, the same shape as an editor's `resource_link`. Used by both front ends. | |
modified
docs/en/explanation/yolo-and-permissions.md +2 -0 | @@ -33,6 +33,8 @@ With `mmConfigs`, one agent can ask another (`/ask-jan`, `mm --ask jan`). Two co | ||
| 33 | 33 | |
| 34 | 34 | Technically, the asked agent is started with `AGENT_YOLO=false`, so it submits every call it would gate through the ACP permission request — the same channel an editor uses. The asking agent receives it and runs it through its own `consent.Gate`: in yolo mode it allows at once; otherwise it asks its own user, in its own front end, and remembers the answer under `jan/bash` so that allowing Jan's shell does not allow yours. The asked agent itself never remembers anything: it is asked again each time, and the asking agent's memory answers. |
| 35 | 35 | |
| 36 | +One refinement keeps the same principle: the user's file may give an asked agent its own `yolo`, in that agent's `mmConfigs` entry (`homer: {path: …, yolo: true}`). It then decides that agent's calls instead of the asking agent's `yolo`: `true` runs them — a coder trusted with edits once the hand-off was approved —, `false` asks for each one even when the asking agent runs freely. It is still the file the user started that decides; the asked agent's own file still does not count. | |
| 37 | + | |
| 36 | 38 | ## Modes that cannot ask |
| 37 | 39 | |
| 38 | 40 | `mm -p` and `mm --ask` have nobody to ask: the first is a one-shot run, the second reads its question from stdin. With `yolo: false` they reject the calls that would ask and say so once on stderr. Allowing them silently would turn `yolo: false` into a setting that depends on how you launched mm, which is exactly the inconsistency this design removes; hanging on an invisible question would block scripts forever. |
| @@ -33,6 +33,8 @@ With `mmConfigs`, one agent can ask another (`/ask-jan`, `mm --ask jan`). Two co | |||
| 33 | 33 | ||
| 34 | Technically, the asked agent is started with `AGENT_YOLO=false`, so it submits every call it would gate through the ACP permission request — the same channel an editor uses. The asking agent receives it and runs it through its own `consent.Gate`: in yolo mode it allows at once; otherwise it asks its own user, in its own front end, and remembers the answer under `jan/bash` so that allowing Jan's shell does not allow yours. The asked agent itself never remembers anything: it is asked again each time, and the asking agent's memory answers. | 34 | Technically, the asked agent is started with `AGENT_YOLO=false`, so it submits every call it would gate through the ACP permission request — the same channel an editor uses. The asking agent receives it and runs it through its own `consent.Gate`: in yolo mode it allows at once; otherwise it asks its own user, in its own front end, and remembers the answer under `jan/bash` so that allowing Jan's shell does not allow yours. The asked agent itself never remembers anything: it is asked again each time, and the asking agent's memory answers. |
| 35 | 35 | ||
| 36 | +One refinement keeps the same principle: the user's file may give an asked agent its own `yolo`, in that agent's `mmConfigs` entry (`homer: {path: …, yolo: true}`). It then decides that agent's calls instead of the asking agent's `yolo`: `true` runs them — a coder trusted with edits once the hand-off was approved —, `false` asks for each one even when the asking agent runs freely. It is still the file the user started that decides; the asked agent's own file still does not count. | ||
| 37 | + | ||
| 36 | ## Modes that cannot ask | 38 | ## Modes that cannot ask |
| 37 | 39 | ||
| 38 | `mm -p` and `mm --ask` have nobody to ask: the first is a one-shot run, the second reads its question from stdin. With `yolo: false` they reject the calls that would ask and say so once on stderr. Allowing them silently would turn `yolo: false` into a setting that depends on how you launched mm, which is exactly the inconsistency this design removes; hanging on an invisible question would block scripts forever. | 40 | `mm -p` and `mm --ask` have nobody to ask: the first is a one-shot run, the second reads its question from stdin. With `yolo: false` they reject the calls that would ask and say so once on stderr. Allowing them silently would turn `yolo: false` into a setting that depends on how you launched mm, which is exactly the inconsistency this design removes; hanging on an invisible question would block scripts forever. |
modified
docs/en/how-to/ask-another-agent.md +1 -1 | @@ -50,7 +50,7 @@ This guide shows how to put a question to another mm agent — another model, an | ||
| 50 | 50 | |
| 51 | 51 | - **Let the other agent see the conversation.** Use `/ask-with-context-lucy <prompt>`: Lucy receives a transcript of the conversation (questions, answers, one line per tool call — never tool outputs) before your question. |
| 52 | 52 | - **In the terminal or the window, by name.** `/ask lucy <prompt>` and `/ask-with-context lucy <prompt>` are the same commands with the agent as an argument. |
| 53 | -- **In an editor (ACP).** The menu lists `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy`; when Lucy wants to run a command, your agent's `yolo` decides: with `yolo: true` it runs, with `yolo: false` the editor's own permission dialog asks you — whatever Lucy's own file says. See [How to confirm tool calls](confirm-tool-calls.md). | |
| 53 | +- **In an editor (ACP).** The menu lists `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy`; when Lucy wants to run a command, your agent's `yolo` decides: with `yolo: true` it runs, with `yolo: false` the editor's own permission dialog asks you — whatever Lucy's own file says. A `yolo` in Lucy's entry of YOUR `mmConfigs` (`lucy: {path: agent.lucy.yaml, yolo: true}`) decides for her instead. See [How to confirm tool calls](confirm-tool-calls.md). | |
| 54 | 54 | - **Stop a long answer.** `/abort`, `Esc` or `Ctrl+C` — nothing is kept from a cancelled ask. |
| 55 | 55 | - **Agents on other providers.** Any mm configuration works: an Ollama agent next to an llmman one is just another `mmConfigs` entry. |
| 56 | 56 | |
| @@ -50,7 +50,7 @@ This guide shows how to put a question to another mm agent — another model, an | |||
| 50 | 50 | ||
| 51 | - **Let the other agent see the conversation.** Use `/ask-with-context-lucy <prompt>`: Lucy receives a transcript of the conversation (questions, answers, one line per tool call — never tool outputs) before your question. | 51 | - **Let the other agent see the conversation.** Use `/ask-with-context-lucy <prompt>`: Lucy receives a transcript of the conversation (questions, answers, one line per tool call — never tool outputs) before your question. |
| 52 | - **In the terminal or the window, by name.** `/ask lucy <prompt>` and `/ask-with-context lucy <prompt>` are the same commands with the agent as an argument. | 52 | - **In the terminal or the window, by name.** `/ask lucy <prompt>` and `/ask-with-context lucy <prompt>` are the same commands with the agent as an argument. |
| 53 | -- **In an editor (ACP).** The menu lists `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy`; when Lucy wants to run a command, your agent's `yolo` decides: with `yolo: true` it runs, with `yolo: false` the editor's own permission dialog asks you — whatever Lucy's own file says. See [How to confirm tool calls](confirm-tool-calls.md). | 53 | +- **In an editor (ACP).** The menu lists `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy`; when Lucy wants to run a command, your agent's `yolo` decides: with `yolo: true` it runs, with `yolo: false` the editor's own permission dialog asks you — whatever Lucy's own file says. A `yolo` in Lucy's entry of YOUR `mmConfigs` (`lucy: {path: agent.lucy.yaml, yolo: true}`) decides for her instead. See [How to confirm tool calls](confirm-tool-calls.md). |
| 54 | - **Stop a long answer.** `/abort`, `Esc` or `Ctrl+C` — nothing is kept from a cancelled ask. | 54 | - **Stop a long answer.** `/abort`, `Esc` or `Ctrl+C` — nothing is kept from a cancelled ask. |
| 55 | - **Agents on other providers.** Any mm configuration works: an Ollama agent next to an llmman one is just another `mmConfigs` entry. | 55 | - **Agents on other providers.** Any mm configuration works: an Ollama agent next to an llmman one is just another `mmConfigs` entry. |
| 56 | 56 | ||
added
docs/en/how-to/inspect-model-traffic.md +62 -0 | new file mode 100644 | ||
| @@ -0,0 +1,62 @@ | ||
| 1 | +# How to see the JSON sent to the model and its answer | |
| 2 | + | |
| 3 | +This guide shows how to keep, for every call to the model, the exact request mm sent and the exact response the server returned — to check a system prompt, the tool definitions, a sampling key, what another agent received, or what a model really answered. It assumes you already run `mm` with a configuration file. | |
| 4 | + | |
| 5 | +## Steps | |
| 6 | + | |
| 7 | +1. Turn the trace on, for one run: | |
| 8 | + | |
| 9 | + ```bash | |
| 10 | + AGENT_TRACE=true mm agent.yaml | |
| 11 | + ``` | |
| 12 | + | |
| 13 | + or for good, in the configuration: | |
| 14 | + | |
| 15 | + ```yaml | |
| 16 | + trace: | |
| 17 | + enabled: true | |
| 18 | + dir: .mm/traces # the default | |
| 19 | + ``` | |
| 20 | + | |
| 21 | + The banner ends with `| trace: /abs/path/.mm/traces` while it is on. | |
| 22 | + | |
| 23 | +2. Ask a question as usual. Each call to the model leaves a pair of files: | |
| 24 | + | |
| 25 | + ```text | |
| 26 | + .mm/traces/20261007-123237.903-bob-0001.request.json | |
| 27 | + .mm/traces/20261007-123237.903-bob-0001.response-200.sse | |
| 28 | + ``` | |
| 29 | + | |
| 30 | + One question can make several calls: one per round trip with the tools, plus a compaction summary. The agent's `acp.name` is in each name, so an agent called through `ask_agent` or `/ask` — it inherits `AGENT_TRACE` — writes its own files beside yours (`…-jan-0001…`). | |
| 31 | + | |
| 32 | +3. Read the request. It is the body exactly as sent, indented: | |
| 33 | + | |
| 34 | + ```bash | |
| 35 | + jq '{model, temperature, stream_options, chat_template_kwargs}' .mm/traces/*-0001.request.json | |
| 36 | + jq -r '.messages[] | "\(.role): \(.content | tostring | .[0:120])"' .mm/traces/*-0001.request.json | |
| 37 | + jq -r '.tools[].function.name' .mm/traces/*-0001.request.json | |
| 38 | + ``` | |
| 39 | + | |
| 40 | +4. Read the response. A streamed one (`.sse`) is the server's lines as they came, one `data: {…}` chunk each; rebuild the answer text with: | |
| 41 | + | |
| 42 | + ```bash | |
| 43 | + grep '^data: {' .mm/traces/*-0001.response-200.sse | sed 's/^data: //' \ | |
| 44 | + | jq -rj '.choices[0].delta.content // empty'; echo | |
| 45 | + ``` | |
| 46 | + | |
| 47 | + and its reasoning with `.choices[0].delta.reasoning_content`, its tool calls with `.choices[0].delta.tool_calls`, its token counts with `.usage` (the last chunk). A response that was not streamed is a `.json` file, indented. An error status is in the name (`.response-400.json`); a call that got no response at all leaves `.response-error.txt`. | |
| 48 | + | |
| 49 | +5. Turn it off when you are done: the files hold your prompts, file contents and command outputs in full. `.mm/` is git-ignored in this repository; add it to your project's `.gitignore` too. | |
| 50 | + | |
| 51 | +## Variants | |
| 52 | + | |
| 53 | +- **In an editor (ACP).** A relative `trace.dir` is in the session's `cwd` — the project the editor opened — not in the directory the editor launched `mm` from. Set `AGENT_TRACE` in the editor's agent settings (`env`), or `trace.enabled` in the file: the editor does not see your shell's variables. | |
| 54 | +- **Somewhere else.** `trace.dir: /tmp/mm-traces` collects every project's calls in one place. | |
| 55 | +- **Only the agents you ask.** Put `trace.enabled: true` in that agent's own file instead of using `AGENT_TRACE`. | |
| 56 | +- **Before the files: what mm adds.** The request already carries what mm adds itself — `stream_options.include_usage`, `return_progress` for a llama.cpp server, `chat_template_kwargs.enable_thinking: false` on a summary — so the trace is also how to check those. | |
| 57 | + | |
| 58 | +## See also | |
| 59 | + | |
| 60 | +- The `trace` keys and the file names: [configuration reference](../reference/configuration.md) | |
| 61 | +- `AGENT_TRACE`: [command-line reference](../reference/cli.md) | |
| 62 | +- Why reasoning arrives in its own field: [Thinking models](../explanation/thinking-models.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | +# How to see the JSON sent to the model and its answer | ||
| 2 | + | ||
| 3 | +This guide shows how to keep, for every call to the model, the exact request mm sent and the exact response the server returned — to check a system prompt, the tool definitions, a sampling key, what another agent received, or what a model really answered. It assumes you already run `mm` with a configuration file. | ||
| 4 | + | ||
| 5 | +## Steps | ||
| 6 | + | ||
| 7 | +1. Turn the trace on, for one run: | ||
| 8 | + | ||
| 9 | + ```bash | ||
| 10 | + AGENT_TRACE=true mm agent.yaml | ||
| 11 | + ``` | ||
| 12 | + | ||
| 13 | + or for good, in the configuration: | ||
| 14 | + | ||
| 15 | + ```yaml | ||
| 16 | + trace: | ||
| 17 | + enabled: true | ||
| 18 | + dir: .mm/traces # the default | ||
| 19 | + ``` | ||
| 20 | + | ||
| 21 | + The banner ends with `| trace: /abs/path/.mm/traces` while it is on. | ||
| 22 | + | ||
| 23 | +2. Ask a question as usual. Each call to the model leaves a pair of files: | ||
| 24 | + | ||
| 25 | + ```text | ||
| 26 | + .mm/traces/20261007-123237.903-bob-0001.request.json | ||
| 27 | + .mm/traces/20261007-123237.903-bob-0001.response-200.sse | ||
| 28 | + ``` | ||
| 29 | + | ||
| 30 | + One question can make several calls: one per round trip with the tools, plus a compaction summary. The agent's `acp.name` is in each name, so an agent called through `ask_agent` or `/ask` — it inherits `AGENT_TRACE` — writes its own files beside yours (`…-jan-0001…`). | ||
| 31 | + | ||
| 32 | +3. Read the request. It is the body exactly as sent, indented: | ||
| 33 | + | ||
| 34 | + ```bash | ||
| 35 | + jq '{model, temperature, stream_options, chat_template_kwargs}' .mm/traces/*-0001.request.json | ||
| 36 | + jq -r '.messages[] | "\(.role): \(.content | tostring | .[0:120])"' .mm/traces/*-0001.request.json | ||
| 37 | + jq -r '.tools[].function.name' .mm/traces/*-0001.request.json | ||
| 38 | + ``` | ||
| 39 | + | ||
| 40 | +4. Read the response. A streamed one (`.sse`) is the server's lines as they came, one `data: {…}` chunk each; rebuild the answer text with: | ||
| 41 | + | ||
| 42 | + ```bash | ||
| 43 | + grep '^data: {' .mm/traces/*-0001.response-200.sse | sed 's/^data: //' \ | ||
| 44 | + | jq -rj '.choices[0].delta.content // empty'; echo | ||
| 45 | + ``` | ||
| 46 | + | ||
| 47 | + and its reasoning with `.choices[0].delta.reasoning_content`, its tool calls with `.choices[0].delta.tool_calls`, its token counts with `.usage` (the last chunk). A response that was not streamed is a `.json` file, indented. An error status is in the name (`.response-400.json`); a call that got no response at all leaves `.response-error.txt`. | ||
| 48 | + | ||
| 49 | +5. Turn it off when you are done: the files hold your prompts, file contents and command outputs in full. `.mm/` is git-ignored in this repository; add it to your project's `.gitignore` too. | ||
| 50 | + | ||
| 51 | +## Variants | ||
| 52 | + | ||
| 53 | +- **In an editor (ACP).** A relative `trace.dir` is in the session's `cwd` — the project the editor opened — not in the directory the editor launched `mm` from. Set `AGENT_TRACE` in the editor's agent settings (`env`), or `trace.enabled` in the file: the editor does not see your shell's variables. | ||
| 54 | +- **Somewhere else.** `trace.dir: /tmp/mm-traces` collects every project's calls in one place. | ||
| 55 | +- **Only the agents you ask.** Put `trace.enabled: true` in that agent's own file instead of using `AGENT_TRACE`. | ||
| 56 | +- **Before the files: what mm adds.** The request already carries what mm adds itself — `stream_options.include_usage`, `return_progress` for a llama.cpp server, `chat_template_kwargs.enable_thinking: false` on a summary — so the trace is also how to check those. | ||
| 57 | + | ||
| 58 | +## See also | ||
| 59 | + | ||
| 60 | +- The `trace` keys and the file names: [configuration reference](../reference/configuration.md) | ||
| 61 | +- `AGENT_TRACE`: [command-line reference](../reference/cli.md) | ||
| 62 | +- Why reasoning arrives in its own field: [Thinking models](../explanation/thinking-models.md) | ||
modified
docs/en/how-to/use-from-an-editor.md +1 -0 | @@ -32,6 +32,7 @@ This guide shows how to host `mm` inside an editor that speaks the Agent Client | ||
| 32 | 32 | - **Compress or restart the conversation.** Send `/compact` or `/new`; editors that list the agent's commands offer both in the slash-command menu. See the [slash commands reference](../reference/slash-commands.md#acp-mode). |
| 33 | 33 | - **Attach a file with `@`.** The editor's picker sends a `resource_link`, passed to the model as `[attached file: <path>]`; a path typed by hand as `@path` is understood the same way (see [`@path`](attach-a-file.md)). The model reads the file with its tools. |
| 34 | 34 | - **Reload the agent without losing the thread.** With `sessions.enabled: true` in the config file, the editor's Reload Agent (Zed) calls `session/load` and the whole conversation, tool calls included, is replayed into the thread. See [Keep sessions and resume them](keep-sessions.md). |
| 35 | +- **See the JSON exchanged with the model.** Turn the trace on, in the config file (`trace: {enabled: true}`) or in the editor's settings (`"env": { "AGENT_CONFIG": "…", "AGENT_TRACE": "true" }` — the editor does not pass your shell's variables). The files land in `<project>/.mm/traces/`, the project being the session's `cwd`; the banner in the ACP logs says `trace: <session cwd>/.mm/traces`. See [How to see the JSON sent to the model](inspect-model-traffic.md). | |
| 35 | 36 | - **Change the name the editor sees.** Add an `acp:` block to the config file; `name` is the identifier the editor logs, `title` the name it may display (leave `title` empty to not send one). The version sent is always the binary's, the one `mm --version` prints — check it with the `initialize` request below. See the [`acp` keys](../reference/configuration.md#acp-keys). |
| 36 | 37 | |
| 37 | 38 | ```yaml |
| @@ -32,6 +32,7 @@ This guide shows how to host `mm` inside an editor that speaks the Agent Client | |||
| 32 | - **Compress or restart the conversation.** Send `/compact` or `/new`; editors that list the agent's commands offer both in the slash-command menu. See the [slash commands reference](../reference/slash-commands.md#acp-mode). | 32 | - **Compress or restart the conversation.** Send `/compact` or `/new`; editors that list the agent's commands offer both in the slash-command menu. See the [slash commands reference](../reference/slash-commands.md#acp-mode). |
| 33 | - **Attach a file with `@`.** The editor's picker sends a `resource_link`, passed to the model as `[attached file: <path>]`; a path typed by hand as `@path` is understood the same way (see [`@path`](attach-a-file.md)). The model reads the file with its tools. | 33 | - **Attach a file with `@`.** The editor's picker sends a `resource_link`, passed to the model as `[attached file: <path>]`; a path typed by hand as `@path` is understood the same way (see [`@path`](attach-a-file.md)). The model reads the file with its tools. |
| 34 | - **Reload the agent without losing the thread.** With `sessions.enabled: true` in the config file, the editor's Reload Agent (Zed) calls `session/load` and the whole conversation, tool calls included, is replayed into the thread. See [Keep sessions and resume them](keep-sessions.md). | 34 | - **Reload the agent without losing the thread.** With `sessions.enabled: true` in the config file, the editor's Reload Agent (Zed) calls `session/load` and the whole conversation, tool calls included, is replayed into the thread. See [Keep sessions and resume them](keep-sessions.md). |
| 35 | +- **See the JSON exchanged with the model.** Turn the trace on, in the config file (`trace: {enabled: true}`) or in the editor's settings (`"env": { "AGENT_CONFIG": "…", "AGENT_TRACE": "true" }` — the editor does not pass your shell's variables). The files land in `<project>/.mm/traces/`, the project being the session's `cwd`; the banner in the ACP logs says `trace: <session cwd>/.mm/traces`. See [How to see the JSON sent to the model](inspect-model-traffic.md). | ||
| 35 | - **Change the name the editor sees.** Add an `acp:` block to the config file; `name` is the identifier the editor logs, `title` the name it may display (leave `title` empty to not send one). The version sent is always the binary's, the one `mm --version` prints — check it with the `initialize` request below. See the [`acp` keys](../reference/configuration.md#acp-keys). | 36 | - **Change the name the editor sees.** Add an `acp:` block to the config file; `name` is the identifier the editor logs, `title` the name it may display (leave `title` empty to not send one). The version sent is always the binary's, the one `mm --version` prints — check it with the `initialize` request below. See the [`acp` keys](../reference/configuration.md#acp-keys). |
| 36 | 37 | ||
| 37 | ```yaml | 38 | ```yaml |
modified
docs/en/how-to/use-the-window-mode.md +1 -0 | @@ -28,6 +28,7 @@ This guide shows how to run mini-me as a single full-screen window — the conve | ||
| 28 | 28 | |
| 29 | 29 | - **Ask before every `bash` and file tool.** Set `yolo: false` in the YAML. The box is replaced by the question `? bash: rm -rf build` and four keys answer it: `a` allow once, `s` allow this tool for the session, `r` (or Esc) reject, `n` never allow this tool in this session. A rejected tool tells the model `[command rejected by the user]`. The default, `yolo: true`, runs tools without asking. See [How to confirm tool calls](confirm-tool-calls.md). |
| 30 | 30 | - **Point the model at a file.** `@path` works exactly as in the line mode: existing paths are attached and echoed as `📎` lines. See [Point the model at a file with `@path`](attach-a-file.md). |
| 31 | +- **See which agent answers.** The header and the label above each answer name the agent: `mm`, or `mm [<acp.title>]` when the config gives a title. While another agent works — `/ask-<name>`, or the `ask_agent` tool — the header names it with its model and `asked by` your agent, and its answer is labelled with its title (or its `mmConfigs` name); then both come back to your agent. See [How to let your agent call other agents](let-the-agent-call-other-agents.md). | |
| 31 | 32 | - **Keep and resume sessions.** With `sessions.enabled: true`, the header shows the session id being saved; `/sessions` and `/resume` work as in the line mode. See [Keep sessions and resume them](keep-sessions.md). |
| 32 | 33 | - **Scroll the conversation.** PgUp/PgDn page; **Tab** moves the focus to the conversation so ↑/↓, Home and End scroll it; End resumes following the end. **Ctrl-L** clears the displayed conversation without touching the history. The mouse wheel scrolls only after **F2** (see below). |
| 33 | 34 | - **Not a terminal.** `mm -tui | tee log.txt`, or any run where stdin or stdout is not a terminal, prints `[tui: stdin or stdout is not a terminal — running the line mode]` and runs the line mode. Nothing else changes. |
| @@ -28,6 +28,7 @@ This guide shows how to run mini-me as a single full-screen window — the conve | |||
| 28 | 28 | ||
| 29 | - **Ask before every `bash` and file tool.** Set `yolo: false` in the YAML. The box is replaced by the question `? bash: rm -rf build` and four keys answer it: `a` allow once, `s` allow this tool for the session, `r` (or Esc) reject, `n` never allow this tool in this session. A rejected tool tells the model `[command rejected by the user]`. The default, `yolo: true`, runs tools without asking. See [How to confirm tool calls](confirm-tool-calls.md). | 29 | - **Ask before every `bash` and file tool.** Set `yolo: false` in the YAML. The box is replaced by the question `? bash: rm -rf build` and four keys answer it: `a` allow once, `s` allow this tool for the session, `r` (or Esc) reject, `n` never allow this tool in this session. A rejected tool tells the model `[command rejected by the user]`. The default, `yolo: true`, runs tools without asking. See [How to confirm tool calls](confirm-tool-calls.md). |
| 30 | - **Point the model at a file.** `@path` works exactly as in the line mode: existing paths are attached and echoed as `📎` lines. See [Point the model at a file with `@path`](attach-a-file.md). | 30 | - **Point the model at a file.** `@path` works exactly as in the line mode: existing paths are attached and echoed as `📎` lines. See [Point the model at a file with `@path`](attach-a-file.md). |
| 31 | +- **See which agent answers.** The header and the label above each answer name the agent: `mm`, or `mm [<acp.title>]` when the config gives a title. While another agent works — `/ask-<name>`, or the `ask_agent` tool — the header names it with its model and `asked by` your agent, and its answer is labelled with its title (or its `mmConfigs` name); then both come back to your agent. See [How to let your agent call other agents](let-the-agent-call-other-agents.md). | ||
| 31 | - **Keep and resume sessions.** With `sessions.enabled: true`, the header shows the session id being saved; `/sessions` and `/resume` work as in the line mode. See [Keep sessions and resume them](keep-sessions.md). | 32 | - **Keep and resume sessions.** With `sessions.enabled: true`, the header shows the session id being saved; `/sessions` and `/resume` work as in the line mode. See [Keep sessions and resume them](keep-sessions.md). |
| 32 | - **Scroll the conversation.** PgUp/PgDn page; **Tab** moves the focus to the conversation so ↑/↓, Home and End scroll it; End resumes following the end. **Ctrl-L** clears the displayed conversation without touching the history. The mouse wheel scrolls only after **F2** (see below). | 33 | - **Scroll the conversation.** PgUp/PgDn page; **Tab** moves the focus to the conversation so ↑/↓, Home and End scroll it; End resumes following the end. **Ctrl-L** clears the displayed conversation without touching the history. The mouse wheel scrolls only after **F2** (see below). |
| 33 | - **Not a terminal.** `mm -tui | tee log.txt`, or any run where stdin or stdout is not a terminal, prints `[tui: stdin or stdout is not a terminal — running the line mode]` and runs the line mode. Nothing else changes. | 34 | - **Not a terminal.** `mm -tui | tee log.txt`, or any run where stdin or stdout is not a terminal, prints `[tui: stdin or stdout is not a terminal — running the line mode]` and runs the line mode. Nothing else changes. |
modified
docs/en/reference/cli.md +4 -3 | @@ -60,7 +60,8 @@ The first existing source wins. | ||
| 60 | 60 | | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. | |
| 61 | 61 | | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. | |
| 62 | 62 | | `AGENT_SESSIONS` | `off` turns session persistence off for this run, whatever `sessions.enabled` says. Set by `/ask` on the agents it starts. | |
| 63 | -| `AGENT_YOLO` | Overrides the `yolo` key: `true` or `false` (any `strconv.ParseBool` spelling; anything else is a configuration error). Set to `false` by `/ask` on the agents it starts, so the asking agent decides. | | |
| 63 | +| `AGENT_TRACE` | Overrides `trace.enabled`: `true` or `false` (any `strconv.ParseBool` spelling; anything else is a configuration error). Inherited by the agents `/ask` and `ask_agent` start. | | |
| 64 | +| `AGENT_YOLO` | Overrides the `yolo` key: `true` or `false` (any `strconv.ParseBool` spelling; anything else is a configuration error). Set to `false` by `/ask` and `ask_agent` on the agents they start, so the asking side decides (the agent's `mmConfigs` entry `yolo`, else the asking agent's). | | |
| 64 | 65 | | `MM_BIN` | Set by `mm` for the commands its `bash` tool runs: the path of the running `mm` binary. | |
| 65 | 66 | | `MM_CONFIG` | Set by `mm` for the commands its `bash` tool runs: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. | |
| 66 | 67 | | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. | |
| @@ -73,7 +74,7 @@ Printed on stdout in terminal mode, on stderr in ACP mode. Dimmed when stdout is | ||
| 73 | 74 | | Line | Content | |
| 74 | 75 | |------|---------| |
| 75 | 76 | | `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), the deprecated `permissions` key. | |
| 76 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`. | | |
| 77 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| trace: …]` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`., and, while `trace.enabled` is on, `trace: <absolute dir>` (`trace: <session cwd>/<dir>` in ACP mode). | | |
| 77 | 78 | | `Agent ready. Commands: …` | Terminal mode only. | |
| 78 | 79 | | `Shortcut: Ctrl+C …` | Terminal mode only. | |
| 79 | 80 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Terminal mode, `sessions.enabled` only: the resolved sessions directory and how many sessions it already holds. | |
| @@ -141,7 +142,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 141 | 142 | | Context (`--ask-with-context`) | stdin. Without `-p`, stdin is `<context>`, a line `---`, then the question. | |
| 142 | 143 | | stdout | The agent's answer, and nothing else, followed by a newline. | |
| 143 | 144 | | stderr | `🗣 asking <agent> (<provider>/<model>)…`, one `🛠️ <agent> · <tool>: <args>` line per tool call, `[ask: …]` on error. | |
| 144 | -| The agent's tool calls | Decided by this configuration's `yolo`: `true` = allowed; `false` = rejected, with the same stderr line as in prompt mode (stdin carried the question, nobody can answer). | | |
| 145 | +| The agent's tool calls | Decided by the agent's `mmConfigs` entry `yolo` when set, else by this configuration's `yolo`: `true` = allowed; `false` = rejected, with the same stderr line as in prompt mode (stdin carried the question, nobody can answer). | | |
| 145 | 146 | | Parent process | Watched every 500 ms: when it is gone, the ask is cancelled and the agent stopped. | |
| 146 | 147 | | Session | None: neither this process nor the agent writes session files. | |
| 147 | 148 | |
| @@ -60,7 +60,8 @@ The first existing source wins. | |||
| 60 | | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. | | 60 | | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. | |
| 61 | | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. | | 61 | | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. | |
| 62 | | `AGENT_SESSIONS` | `off` turns session persistence off for this run, whatever `sessions.enabled` says. Set by `/ask` on the agents it starts. | | 62 | | `AGENT_SESSIONS` | `off` turns session persistence off for this run, whatever `sessions.enabled` says. Set by `/ask` on the agents it starts. | |
| 63 | -| `AGENT_YOLO` | Overrides the `yolo` key: `true` or `false` (any `strconv.ParseBool` spelling; anything else is a configuration error). Set to `false` by `/ask` on the agents it starts, so the asking agent decides. | | 63 | +| `AGENT_TRACE` | Overrides `trace.enabled`: `true` or `false` (any `strconv.ParseBool` spelling; anything else is a configuration error). Inherited by the agents `/ask` and `ask_agent` start. | |
| 64 | +| `AGENT_YOLO` | Overrides the `yolo` key: `true` or `false` (any `strconv.ParseBool` spelling; anything else is a configuration error). Set to `false` by `/ask` and `ask_agent` on the agents they start, so the asking side decides (the agent's `mmConfigs` entry `yolo`, else the asking agent's). | | ||
| 64 | | `MM_BIN` | Set by `mm` for the commands its `bash` tool runs: the path of the running `mm` binary. | | 65 | | `MM_BIN` | Set by `mm` for the commands its `bash` tool runs: the path of the running `mm` binary. | |
| 65 | | `MM_CONFIG` | Set by `mm` for the commands its `bash` tool runs: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. | | 66 | | `MM_CONFIG` | Set by `mm` for the commands its `bash` tool runs: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. | |
| 66 | | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. | | 67 | | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. | |
| @@ -73,7 +74,7 @@ Printed on stdout in terminal mode, on stderr in ACP mode. Dimmed when stdout is | |||
| 73 | | Line | Content | | 74 | | Line | Content | |
| 74 | |------|---------| | 75 | |------|---------| |
| 75 | | `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), the deprecated `permissions` key. | | 76 | | `[warning: …]` | Zero or more probe warnings: server unreachable, model not in the server's list, no chat template, compression that can never trigger, no tool declared at all, an `mmConfigs` file not found (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), the deprecated `permissions` key. | |
| 76 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`. | | 77 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| trace: …]` | Version (see above), configuration path, provider name, model identifier, context window with its origin (`config`, `/props`) or `unknown`, declared tool names, number of skills found, `yolo: on` or `yolo: off (tools ask)`., and, while `trace.enabled` is on, `trace: <absolute dir>` (`trace: <session cwd>/<dir>` in ACP mode). | |
| 77 | | `Agent ready. Commands: …` | Terminal mode only. | | 78 | | `Agent ready. Commands: …` | Terminal mode only. | |
| 78 | | `Shortcut: Ctrl+C …` | Terminal mode only. | | 79 | | `Shortcut: Ctrl+C …` | Terminal mode only. | |
| 79 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Terminal mode, `sessions.enabled` only: the resolved sessions directory and how many sessions it already holds. | | 80 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Terminal mode, `sessions.enabled` only: the resolved sessions directory and how many sessions it already holds. | |
| @@ -141,7 +142,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 141 | | Context (`--ask-with-context`) | stdin. Without `-p`, stdin is `<context>`, a line `---`, then the question. | | 142 | | Context (`--ask-with-context`) | stdin. Without `-p`, stdin is `<context>`, a line `---`, then the question. | |
| 142 | | stdout | The agent's answer, and nothing else, followed by a newline. | | 143 | | stdout | The agent's answer, and nothing else, followed by a newline. | |
| 143 | | stderr | `🗣 asking <agent> (<provider>/<model>)…`, one `🛠️ <agent> · <tool>: <args>` line per tool call, `[ask: …]` on error. | | 144 | | stderr | `🗣 asking <agent> (<provider>/<model>)…`, one `🛠️ <agent> · <tool>: <args>` line per tool call, `[ask: …]` on error. | |
| 144 | -| The agent's tool calls | Decided by this configuration's `yolo`: `true` = allowed; `false` = rejected, with the same stderr line as in prompt mode (stdin carried the question, nobody can answer). | | 145 | +| The agent's tool calls | Decided by the agent's `mmConfigs` entry `yolo` when set, else by this configuration's `yolo`: `true` = allowed; `false` = rejected, with the same stderr line as in prompt mode (stdin carried the question, nobody can answer). | |
| 145 | | Parent process | Watched every 500 ms: when it is gone, the ask is cancelled and the agent stopped. | | 146 | | Parent process | Watched every 500 ms: when it is gone, the ask is cancelled and the agent stopped. | |
| 146 | | Session | None: neither this process nor the agent writes session files. | | 147 | | Session | None: neither this process nor the agent writes session files. | |
| 147 | 148 | ||
modified
docs/en/reference/configuration.md +12 -1 | @@ -28,10 +28,11 @@ | ||
| 28 | 28 | | `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. | |
| 29 | 29 | | `context` | map | see below | Automatic history compression. | |
| 30 | 30 | | `sessions` | map | see below | Persistent, per-project sessions. | |
| 31 | +| `trace` | map | see below | Write every request to the model and every response to files. | | |
| 31 | 32 | | `acp` | map | see below | How `mm -acp` introduces itself to the editor. | |
| 32 | 33 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | |
| 33 | 34 | | `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`. | |
| 34 | -| `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` runs with `AGENT_YOLO=false` and submits its calls to the asking agent, whose `yolo` decides. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). | | |
| 35 | +| `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). | | |
| 35 | 36 | | `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`. | |
| 36 | 37 | | `mmConfigs` | map name → path, or name → `{path, description, yolo}` | 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). `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. | |
| 37 | 38 | |
| @@ -54,6 +55,15 @@ | ||
| 54 | 55 | | `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. | |
| 55 | 56 | | `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. | |
| 56 | 57 | |
| 58 | +## `trace` keys | |
| 59 | + | |
| 60 | +| Key | Type | Default | Description | | |
| 61 | +|-----|------|---------|-------------| | |
| 62 | +| `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). | | |
| 63 | +| `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. | | |
| 64 | + | |
| 65 | +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`. | |
| 66 | + | |
| 57 | 67 | ## `acp` keys |
| 58 | 68 | |
| 59 | 69 | | Key | Type | Default | Description | |
| @@ -113,6 +123,7 @@ Reported as `[config error: <path>: <message>]`, exit code 1. | ||
| 113 | 123 | | `context.summaryMaxTokens must be > 0` | zero or negative | |
| 114 | 124 | | `context.maxMessages must be >= 0` | negative value | |
| 115 | 125 | | `sessions.dir must not be empty` | empty string | |
| 126 | +| `trace.dir must not be empty` | empty string with the trace on | | |
| 116 | 127 | | `acp.name must not be empty` | empty or blank string | |
| 117 | 128 | | `lsp.servers[N].name must not be empty` | a server without `name` | |
| 118 | 129 | | `lsp.servers[N].command must not be empty` | a server without `command` | |
| @@ -28,10 +28,11 @@ | |||
| 28 | | `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. | | 28 | | `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. | |
| 29 | | `context` | map | see below | Automatic history compression. | | 29 | | `context` | map | see below | Automatic history compression. | |
| 30 | | `sessions` | map | see below | Persistent, per-project sessions. | | 30 | | `sessions` | map | see below | Persistent, per-project sessions. | |
| 31 | +| `trace` | map | see below | Write every request to the model and every response to files. | | ||
| 31 | | `acp` | map | see below | How `mm -acp` introduces itself to the editor. | | 32 | | `acp` | map | see below | How `mm -acp` introduces itself to the editor. | |
| 32 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | | 33 | | `lsp` | map | see below | Language servers behind the `lsp` tool. No server listed = no tool. | |
| 33 | | `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`. | | 34 | | `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`. | |
| 34 | -| `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` runs with `AGENT_YOLO=false` and submits its calls to the asking agent, whose `yolo` decides. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). | | 35 | +| `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). | |
| 35 | | `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`. | | 36 | | `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`. | |
| 36 | | `mmConfigs` | map name → path, or name → `{path, description, yolo}` | 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). `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. | | 37 | | `mmConfigs` | map name → path, or name → `{path, description, yolo}` | 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). `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. | |
| 37 | 38 | ||
| @@ -54,6 +55,15 @@ | |||
| 54 | | `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. | | 55 | | `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. | |
| 55 | | `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. | | 56 | | `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. | |
| 56 | 57 | ||
| 58 | +## `trace` keys | ||
| 59 | + | ||
| 60 | +| Key | Type | Default | Description | | ||
| 61 | +|-----|------|---------|-------------| | ||
| 62 | +| `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). | | ||
| 63 | +| `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. | | ||
| 64 | + | ||
| 65 | +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`. | ||
| 66 | + | ||
| 57 | ## `acp` keys | 67 | ## `acp` keys |
| 58 | 68 | ||
| 59 | | Key | Type | Default | Description | | 69 | | Key | Type | Default | Description | |
| @@ -113,6 +123,7 @@ Reported as `[config error: <path>: <message>]`, exit code 1. | |||
| 113 | | `context.summaryMaxTokens must be > 0` | zero or negative | | 123 | | `context.summaryMaxTokens must be > 0` | zero or negative | |
| 114 | | `context.maxMessages must be >= 0` | negative value | | 124 | | `context.maxMessages must be >= 0` | negative value | |
| 115 | | `sessions.dir must not be empty` | empty string | | 125 | | `sessions.dir must not be empty` | empty string | |
| 126 | +| `trace.dir must not be empty` | empty string with the trace on | | ||
| 116 | | `acp.name must not be empty` | empty or blank string | | 127 | | `acp.name must not be empty` | empty or blank string | |
| 117 | | `lsp.servers[N].name must not be empty` | a server without `name` | | 128 | | `lsp.servers[N].name must not be empty` | a server without `name` | |
| 118 | | `lsp.servers[N].command must not be empty` | a server without `command` | | 129 | | `lsp.servers[N].command must not be empty` | a server without `command` | |
modified
docs/fr/README.md +2 -1 | @@ -14,6 +14,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | ||
| 14 | 14 | - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demo-llmman/` |
| 15 | 15 | - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demo-ollama/` |
| 16 | 16 | - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>` |
| 17 | +- [Voir le JSON envoyé au modèle et sa réponse](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, lire les fichiers avec jq | |
| 17 | 18 | - [Laisser votre agent appeler d'autres agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, descriptions dans `mmConfigs`, `yolo` par agent |
| 18 | 19 | - [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context` |
| 19 | 20 | - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md) |
| @@ -61,7 +62,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | ||
| 61 | 62 | - [L'outil `lsp`](explanation/lsp-tool.md) — un seul outil avec un champ `action`, un champ `symbol` plutôt que des colonnes, des fichiers synchronisés depuis le disque, un renommage soumis à permission |
| 62 | 63 | - [L'outil `fetch_content`](explanation/fetch-content-tool.md) — pourquoi un outil intégré à côté du `fetch` MCP, un convertisseur maison, le robots.txt, des erreurs numérotées en texte, les limites connues |
| 63 | 64 | - [Modèles à raisonnement](explanation/thinking-models.md) — d'où vient le raisonnement d'un modèle, pourquoi `mm` le lit sous Genkit, et pourquoi le lire garde le chien de garde en vie |
| 64 | -- [Le mode yolo et les permissions](explanation/yolo-and-permissions.md) — pourquoi les outils s'exécutent sans demander par défaut, exister ou demander, quatre réponses dans un seul paquet, pourquoi l'agent qui interroge décide | |
| 65 | +- [Le mode yolo et les permissions](explanation/yolo-and-permissions.md) — pourquoi les outils s'exécutent sans demander par défaut, exister ou demander, quatre réponses dans un seul paquet, pourquoi l'agent qui interroge décide (et un `yolo` par agent dans `mmConfigs`) | |
| 65 | 66 | - [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, `ask_agent` et le `yolo` par agent, la limite de profondeur |
| 66 | 67 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 67 | 68 | |
| @@ -14,6 +14,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | |||
| 14 | - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demo-llmman/` | 14 | - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demo-llmman/` |
| 15 | - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demo-ollama/` | 15 | - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demo-ollama/` |
| 16 | - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>` | 16 | - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>` |
| 17 | +- [Voir le JSON envoyé au modèle et sa réponse](how-to/inspect-model-traffic.md) — `trace`, `AGENT_TRACE`, lire les fichiers avec jq | ||
| 17 | - [Laisser votre agent appeler d'autres agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, descriptions dans `mmConfigs`, `yolo` par agent | 18 | - [Laisser votre agent appeler d'autres agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, descriptions dans `mmConfigs`, `yolo` par agent |
| 18 | - [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context` | 19 | - [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context` |
| 19 | - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md) | 20 | - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md) |
| @@ -61,7 +62,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | |||
| 61 | - [L'outil `lsp`](explanation/lsp-tool.md) — un seul outil avec un champ `action`, un champ `symbol` plutôt que des colonnes, des fichiers synchronisés depuis le disque, un renommage soumis à permission | 62 | - [L'outil `lsp`](explanation/lsp-tool.md) — un seul outil avec un champ `action`, un champ `symbol` plutôt que des colonnes, des fichiers synchronisés depuis le disque, un renommage soumis à permission |
| 62 | - [L'outil `fetch_content`](explanation/fetch-content-tool.md) — pourquoi un outil intégré à côté du `fetch` MCP, un convertisseur maison, le robots.txt, des erreurs numérotées en texte, les limites connues | 63 | - [L'outil `fetch_content`](explanation/fetch-content-tool.md) — pourquoi un outil intégré à côté du `fetch` MCP, un convertisseur maison, le robots.txt, des erreurs numérotées en texte, les limites connues |
| 63 | - [Modèles à raisonnement](explanation/thinking-models.md) — d'où vient le raisonnement d'un modèle, pourquoi `mm` le lit sous Genkit, et pourquoi le lire garde le chien de garde en vie | 64 | - [Modèles à raisonnement](explanation/thinking-models.md) — d'où vient le raisonnement d'un modèle, pourquoi `mm` le lit sous Genkit, et pourquoi le lire garde le chien de garde en vie |
| 64 | -- [Le mode yolo et les permissions](explanation/yolo-and-permissions.md) — pourquoi les outils s'exécutent sans demander par défaut, exister ou demander, quatre réponses dans un seul paquet, pourquoi l'agent qui interroge décide | 65 | +- [Le mode yolo et les permissions](explanation/yolo-and-permissions.md) — pourquoi les outils s'exécutent sans demander par défaut, exister ou demander, quatre réponses dans un seul paquet, pourquoi l'agent qui interroge décide (et un `yolo` par agent dans `mmConfigs`) |
| 65 | - [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, `ask_agent` et le `yolo` par agent, la limite de profondeur | 66 | - [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, `ask_agent` et le `yolo` par agent, la limite de profondeur |
| 66 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) | 67 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 67 | 68 | ||
modified
docs/fr/explanation/architecture.md +3 -3 | @@ -9,13 +9,13 @@ Le point d'entrée, `main.go`, ne fait que du câblage : il charge les réglages | ||
| 9 | 9 | | Paquet | Responsabilité | |
| 10 | 10 | |--------|----------------| |
| 11 | 11 | | `config` | Tous les réglages dans une structure, avec des valeurs par défaut intégrées surchargées par le YAML et quelques variables d'environnement. | |
| 12 | -| `engine` | La connexion au serveur de modèle et la génération elle-même : streaming, watchdog, nouvelle tentative sans streaming, requêtes de résumé, le registre des fournisseurs, et le raisonnement qu'un modèle diffuse à côté de sa réponse (voir [Modèles à raisonnement](thinking-models.md)). | | |
| 13 | -| `tools` | Les outils intégrés que le modèle peut appeler. | | |
| 12 | +| `engine` | La connexion au serveur de modèle et la génération elle-même : streaming, watchdog, nouvelle tentative sans streaming, requêtes de résumé, le registre des fournisseurs, le raisonnement qu'un modèle diffuse à côté de sa réponse (voir [Modèles à raisonnement](thinking-models.md)), et la trace optionnelle de chaque requête et réponse (`trace.go`). | | |
| 13 | +| `tools` | Les outils intégrés que le modèle peut appeler, `ask_agent` compris (il interroge un agent de `mmConfigs` via `ask`). | | |
| 14 | 14 | | `fileedit` | L'édition de fichiers par remplacement exact, les règles derrière `edit_file`. | |
| 15 | 15 | | `lsp` | Le client de serveur de langage derrière l'outil `lsp` : démarre les serveurs configurés au premier usage (clé `lsp.servers`), synchronise les fichiers depuis le disque, demande définition, références, hover, symboles, diagnostics et renommage, et met les réponses en forme `chemin:ligne:colonne <ligne>`. Construit sur `go.lsp.dev/protocol` et `go.lsp.dev/jsonrpc2`. | |
| 16 | 16 | | `skills` | La découverte des procédures markdown et le rendu du catalogue de `read_skill`. | |
| 17 | 17 | | `command` | L'unique table des commandes slash : nom, description, façades qui l'offrent, argument éventuel. Le REPL s'y branche, la façade ACP en annonce la tranche Éditeur, le sélecteur `/` de la fenêtre la liste. | |
| 18 | -| `ask` | `/ask-<nom>` : démarre un autre agent mm (clé `mmConfigs`) comme enfant `mm -acp`, lui parle en client ACP, affiche sa réponse et ses appels d'outils via le `ui.Sink` de la façade qui pose la question, et renvoie l'échange pour l'historique. Voir [Interroger un autre agent](asking-another-agent.md). | | |
| 18 | +| `ask` | `/ask-<nom>` : démarre un autre agent mm (clé `mmConfigs`) comme enfant `mm -acp`, lui parle en client ACP, affiche sa réponse et ses appels d'outils via le `ui.Sink` de la façade qui pose la question, et renvoie l'échange pour l'historique ; le même pool sert l'outil `ask_agent` et `mm --ask`. Voir [Interroger un autre agent](asking-another-agent.md). | | |
| 19 | 19 | | `clipboard` | Met du texte dans le presse-papiers système : un outil natif du `PATH` d'abord (`pbcopy`, `wl-copy`, `xclip`, `xsel`), OSC 52 en repli, et il dit lequel a servi. Utilisé par `/copy` dans le REPL et la fenêtre. | |
| 20 | 20 | | `store` | Fichiers de session par projet (`<dir>/<id>.json`, réécriture complète, atomique) : création, sauvegarde, chargement, liste, plus récente. Utilisé par les deux façades quand `sessions.enabled`. | |
| 21 | 21 | | `mention` | La notation `@chemin` d'une question : les chemins existants deviennent des lignes `[attached file: …]`, la même forme que le `resource_link` d'un éditeur. Utilisé par les deux façades. | |
| @@ -9,13 +9,13 @@ Le point d'entrée, `main.go`, ne fait que du câblage : il charge les réglages | |||
| 9 | | Paquet | Responsabilité | | 9 | | Paquet | Responsabilité | |
| 10 | |--------|----------------| | 10 | |--------|----------------| |
| 11 | | `config` | Tous les réglages dans une structure, avec des valeurs par défaut intégrées surchargées par le YAML et quelques variables d'environnement. | | 11 | | `config` | Tous les réglages dans une structure, avec des valeurs par défaut intégrées surchargées par le YAML et quelques variables d'environnement. | |
| 12 | -| `engine` | La connexion au serveur de modèle et la génération elle-même : streaming, watchdog, nouvelle tentative sans streaming, requêtes de résumé, le registre des fournisseurs, et le raisonnement qu'un modèle diffuse à côté de sa réponse (voir [Modèles à raisonnement](thinking-models.md)). | | 12 | +| `engine` | La connexion au serveur de modèle et la génération elle-même : streaming, watchdog, nouvelle tentative sans streaming, requêtes de résumé, le registre des fournisseurs, le raisonnement qu'un modèle diffuse à côté de sa réponse (voir [Modèles à raisonnement](thinking-models.md)), et la trace optionnelle de chaque requête et réponse (`trace.go`). | |
| 13 | -| `tools` | Les outils intégrés que le modèle peut appeler. | | 13 | +| `tools` | Les outils intégrés que le modèle peut appeler, `ask_agent` compris (il interroge un agent de `mmConfigs` via `ask`). | |
| 14 | | `fileedit` | L'édition de fichiers par remplacement exact, les règles derrière `edit_file`. | | 14 | | `fileedit` | L'édition de fichiers par remplacement exact, les règles derrière `edit_file`. | |
| 15 | | `lsp` | Le client de serveur de langage derrière l'outil `lsp` : démarre les serveurs configurés au premier usage (clé `lsp.servers`), synchronise les fichiers depuis le disque, demande définition, références, hover, symboles, diagnostics et renommage, et met les réponses en forme `chemin:ligne:colonne <ligne>`. Construit sur `go.lsp.dev/protocol` et `go.lsp.dev/jsonrpc2`. | | 15 | | `lsp` | Le client de serveur de langage derrière l'outil `lsp` : démarre les serveurs configurés au premier usage (clé `lsp.servers`), synchronise les fichiers depuis le disque, demande définition, références, hover, symboles, diagnostics et renommage, et met les réponses en forme `chemin:ligne:colonne <ligne>`. Construit sur `go.lsp.dev/protocol` et `go.lsp.dev/jsonrpc2`. | |
| 16 | | `skills` | La découverte des procédures markdown et le rendu du catalogue de `read_skill`. | | 16 | | `skills` | La découverte des procédures markdown et le rendu du catalogue de `read_skill`. | |
| 17 | | `command` | L'unique table des commandes slash : nom, description, façades qui l'offrent, argument éventuel. Le REPL s'y branche, la façade ACP en annonce la tranche Éditeur, le sélecteur `/` de la fenêtre la liste. | | 17 | | `command` | L'unique table des commandes slash : nom, description, façades qui l'offrent, argument éventuel. Le REPL s'y branche, la façade ACP en annonce la tranche Éditeur, le sélecteur `/` de la fenêtre la liste. | |
| 18 | -| `ask` | `/ask-<nom>` : démarre un autre agent mm (clé `mmConfigs`) comme enfant `mm -acp`, lui parle en client ACP, affiche sa réponse et ses appels d'outils via le `ui.Sink` de la façade qui pose la question, et renvoie l'échange pour l'historique. Voir [Interroger un autre agent](asking-another-agent.md). | | 18 | +| `ask` | `/ask-<nom>` : démarre un autre agent mm (clé `mmConfigs`) comme enfant `mm -acp`, lui parle en client ACP, affiche sa réponse et ses appels d'outils via le `ui.Sink` de la façade qui pose la question, et renvoie l'échange pour l'historique ; le même pool sert l'outil `ask_agent` et `mm --ask`. Voir [Interroger un autre agent](asking-another-agent.md). | |
| 19 | | `clipboard` | Met du texte dans le presse-papiers système : un outil natif du `PATH` d'abord (`pbcopy`, `wl-copy`, `xclip`, `xsel`), OSC 52 en repli, et il dit lequel a servi. Utilisé par `/copy` dans le REPL et la fenêtre. | | 19 | | `clipboard` | Met du texte dans le presse-papiers système : un outil natif du `PATH` d'abord (`pbcopy`, `wl-copy`, `xclip`, `xsel`), OSC 52 en repli, et il dit lequel a servi. Utilisé par `/copy` dans le REPL et la fenêtre. | |
| 20 | | `store` | Fichiers de session par projet (`<dir>/<id>.json`, réécriture complète, atomique) : création, sauvegarde, chargement, liste, plus récente. Utilisé par les deux façades quand `sessions.enabled`. | | 20 | | `store` | Fichiers de session par projet (`<dir>/<id>.json`, réécriture complète, atomique) : création, sauvegarde, chargement, liste, plus récente. Utilisé par les deux façades quand `sessions.enabled`. | |
| 21 | | `mention` | La notation `@chemin` d'une question : les chemins existants deviennent des lignes `[attached file: …]`, la même forme que le `resource_link` d'un éditeur. Utilisé par les deux façades. | | 21 | | `mention` | La notation `@chemin` d'une question : les chemins existants deviennent des lignes `[attached file: …]`, la même forme que le `resource_link` d'un éditeur. Utilisé par les deux façades. | |
modified
docs/fr/explanation/yolo-and-permissions.md +2 -0 | @@ -33,6 +33,8 @@ Avec `mmConfigs`, un agent peut en interroger un autre (`/ask-jan`, `mm --ask ja | ||
| 33 | 33 | |
| 34 | 34 | Techniquement, l'agent interrogé est lancé avec `AGENT_YOLO=false` : il soumet donc chaque appel qu'il filtrerait par la requête de permission ACP — le même canal qu'utilise un éditeur. L'agent qui interroge la reçoit et la fait passer par son propre `consent.Gate` : en mode yolo, il autorise aussitôt ; sinon, il demande à son propre utilisateur, dans sa propre façade, et mémorise la réponse sous `jan/bash`, pour qu'autoriser le shell de Jan n'autorise pas le vôtre. L'agent interrogé, lui, ne retient rien : on lui redemande à chaque fois, et c'est la mémoire de l'agent qui interroge qui répond. |
| 35 | 35 | |
| 36 | +Un raffinement garde le même principe : le fichier de l'utilisateur peut donner à un agent interrogé son propre `yolo`, dans l'entrée `mmConfigs` de cet agent (`homer: {path: …, yolo: true}`). Il décide alors des appels de cet agent à la place du `yolo` de l'agent qui interroge : `true` les exécute — un codeur à qui l'on confie les modifications une fois le passage de main approuvé —, `false` demande pour chacun même quand l'agent qui interroge s'exécute librement. C'est toujours le fichier lancé par l'utilisateur qui décide ; le propre fichier de l'agent interrogé ne compte toujours pas. | |
| 37 | + | |
| 36 | 38 | ## Les modes qui ne peuvent pas demander |
| 37 | 39 | |
| 38 | 40 | `mm -p` et `mm --ask` n'ont personne à qui demander : le premier est une exécution unique, le second lit sa question sur stdin. Avec `yolo: false`, ils refusent les appels qui demanderaient et le disent une fois sur stderr. Les autoriser en silence ferait de `yolo: false` un réglage qui dépend de la façon dont on a lancé mm, exactement l'incohérence que cette conception supprime ; rester bloqué sur une question invisible bloquerait les scripts pour toujours. |
| @@ -33,6 +33,8 @@ Avec `mmConfigs`, un agent peut en interroger un autre (`/ask-jan`, `mm --ask ja | |||
| 33 | 33 | ||
| 34 | Techniquement, l'agent interrogé est lancé avec `AGENT_YOLO=false` : il soumet donc chaque appel qu'il filtrerait par la requête de permission ACP — le même canal qu'utilise un éditeur. L'agent qui interroge la reçoit et la fait passer par son propre `consent.Gate` : en mode yolo, il autorise aussitôt ; sinon, il demande à son propre utilisateur, dans sa propre façade, et mémorise la réponse sous `jan/bash`, pour qu'autoriser le shell de Jan n'autorise pas le vôtre. L'agent interrogé, lui, ne retient rien : on lui redemande à chaque fois, et c'est la mémoire de l'agent qui interroge qui répond. | 34 | Techniquement, l'agent interrogé est lancé avec `AGENT_YOLO=false` : il soumet donc chaque appel qu'il filtrerait par la requête de permission ACP — le même canal qu'utilise un éditeur. L'agent qui interroge la reçoit et la fait passer par son propre `consent.Gate` : en mode yolo, il autorise aussitôt ; sinon, il demande à son propre utilisateur, dans sa propre façade, et mémorise la réponse sous `jan/bash`, pour qu'autoriser le shell de Jan n'autorise pas le vôtre. L'agent interrogé, lui, ne retient rien : on lui redemande à chaque fois, et c'est la mémoire de l'agent qui interroge qui répond. |
| 35 | 35 | ||
| 36 | +Un raffinement garde le même principe : le fichier de l'utilisateur peut donner à un agent interrogé son propre `yolo`, dans l'entrée `mmConfigs` de cet agent (`homer: {path: …, yolo: true}`). Il décide alors des appels de cet agent à la place du `yolo` de l'agent qui interroge : `true` les exécute — un codeur à qui l'on confie les modifications une fois le passage de main approuvé —, `false` demande pour chacun même quand l'agent qui interroge s'exécute librement. C'est toujours le fichier lancé par l'utilisateur qui décide ; le propre fichier de l'agent interrogé ne compte toujours pas. | ||
| 37 | + | ||
| 36 | ## Les modes qui ne peuvent pas demander | 38 | ## Les modes qui ne peuvent pas demander |
| 37 | 39 | ||
| 38 | `mm -p` et `mm --ask` n'ont personne à qui demander : le premier est une exécution unique, le second lit sa question sur stdin. Avec `yolo: false`, ils refusent les appels qui demanderaient et le disent une fois sur stderr. Les autoriser en silence ferait de `yolo: false` un réglage qui dépend de la façon dont on a lancé mm, exactement l'incohérence que cette conception supprime ; rester bloqué sur une question invisible bloquerait les scripts pour toujours. | 40 | `mm -p` et `mm --ask` n'ont personne à qui demander : le premier est une exécution unique, le second lit sa question sur stdin. Avec `yolo: false`, ils refusent les appels qui demanderaient et le disent une fois sur stderr. Les autoriser en silence ferait de `yolo: false` un réglage qui dépend de la façon dont on a lancé mm, exactement l'incohérence que cette conception supprime ; rester bloqué sur une question invisible bloquerait les scripts pour toujours. |
modified
docs/fr/how-to/ask-another-agent.md +1 -1 | @@ -50,7 +50,7 @@ Ce guide montre comment poser une question à un autre agent mm — un autre mod | ||
| 50 | 50 | |
| 51 | 51 | - **Laisser l'autre agent voir la conversation.** Utilisez `/ask-with-context-lucy <prompt>` : Lucy reçoit une transcription de la conversation (questions, réponses, une ligne par appel d'outil — jamais les sorties d'outils) avant votre question. |
| 52 | 52 | - **Dans le terminal ou la fenêtre, par nom.** `/ask lucy <prompt>` et `/ask-with-context lucy <prompt>` sont les mêmes commandes, avec l'agent en argument. |
| 53 | -- **Dans un éditeur (ACP).** Le menu liste `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy` ; quand Lucy veut lancer une commande, c'est le `yolo` de votre agent qui décide : avec `yolo: true` elle s'exécute, avec `yolo: false` c'est la boîte de dialogue de permission de l'éditeur qui vous le demande — quoi que dise le fichier de Lucy. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). | |
| 53 | +- **Dans un éditeur (ACP).** Le menu liste `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy` ; quand Lucy veut lancer une commande, c'est le `yolo` de votre agent qui décide : avec `yolo: true` elle s'exécute, avec `yolo: false` c'est la boîte de dialogue de permission de l'éditeur qui vous le demande — quoi que dise le fichier de Lucy. Un `yolo` dans l'entrée de Lucy de VOS `mmConfigs` (`lucy: {path: agent.lucy.yaml, yolo: true}`) décide pour elle à la place. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). | |
| 54 | 54 | - **Arrêter une longue réponse.** `/abort`, `Échap` ou `Ctrl+C` — rien n'est gardé d'une demande annulée. |
| 55 | 55 | - **Des agents sur d'autres fournisseurs.** N'importe quelle configuration mm convient : un agent Ollama à côté d'un agent llmman n'est qu'une entrée de plus dans `mmConfigs`. |
| 56 | 56 | |
| @@ -50,7 +50,7 @@ Ce guide montre comment poser une question à un autre agent mm — un autre mod | |||
| 50 | 50 | ||
| 51 | - **Laisser l'autre agent voir la conversation.** Utilisez `/ask-with-context-lucy <prompt>` : Lucy reçoit une transcription de la conversation (questions, réponses, une ligne par appel d'outil — jamais les sorties d'outils) avant votre question. | 51 | - **Laisser l'autre agent voir la conversation.** Utilisez `/ask-with-context-lucy <prompt>` : Lucy reçoit une transcription de la conversation (questions, réponses, une ligne par appel d'outil — jamais les sorties d'outils) avant votre question. |
| 52 | - **Dans le terminal ou la fenêtre, par nom.** `/ask lucy <prompt>` et `/ask-with-context lucy <prompt>` sont les mêmes commandes, avec l'agent en argument. | 52 | - **Dans le terminal ou la fenêtre, par nom.** `/ask lucy <prompt>` et `/ask-with-context lucy <prompt>` sont les mêmes commandes, avec l'agent en argument. |
| 53 | -- **Dans un éditeur (ACP).** Le menu liste `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy` ; quand Lucy veut lancer une commande, c'est le `yolo` de votre agent qui décide : avec `yolo: true` elle s'exécute, avec `yolo: false` c'est la boîte de dialogue de permission de l'éditeur qui vous le demande — quoi que dise le fichier de Lucy. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). | 53 | +- **Dans un éditeur (ACP).** Le menu liste `/ask-jan`, `/ask-with-context-jan`, `/ask-lucy`, `/ask-with-context-lucy` ; quand Lucy veut lancer une commande, c'est le `yolo` de votre agent qui décide : avec `yolo: true` elle s'exécute, avec `yolo: false` c'est la boîte de dialogue de permission de l'éditeur qui vous le demande — quoi que dise le fichier de Lucy. Un `yolo` dans l'entrée de Lucy de VOS `mmConfigs` (`lucy: {path: agent.lucy.yaml, yolo: true}`) décide pour elle à la place. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). |
| 54 | - **Arrêter une longue réponse.** `/abort`, `Échap` ou `Ctrl+C` — rien n'est gardé d'une demande annulée. | 54 | - **Arrêter une longue réponse.** `/abort`, `Échap` ou `Ctrl+C` — rien n'est gardé d'une demande annulée. |
| 55 | - **Des agents sur d'autres fournisseurs.** N'importe quelle configuration mm convient : un agent Ollama à côté d'un agent llmman n'est qu'une entrée de plus dans `mmConfigs`. | 55 | - **Des agents sur d'autres fournisseurs.** N'importe quelle configuration mm convient : un agent Ollama à côté d'un agent llmman n'est qu'une entrée de plus dans `mmConfigs`. |
| 56 | 56 | ||
added
docs/fr/how-to/inspect-model-traffic.md +62 -0 | new file mode 100644 | ||
| @@ -0,0 +1,62 @@ | ||
| 1 | +# Comment voir le JSON envoyé au modèle et sa réponse | |
| 2 | + | |
| 3 | +Ce guide montre comment garder, pour chaque appel au modèle, la requête exacte envoyée par mm et la réponse exacte renvoyée par le serveur — pour vérifier un prompt système, les définitions d'outils, une clé d'échantillonnage, ce qu'un autre agent a reçu, ou ce qu'un modèle a vraiment répondu. Il suppose que vous lancez déjà `mm` avec un fichier de configuration. | |
| 4 | + | |
| 5 | +## Étapes | |
| 6 | + | |
| 7 | +1. Activez la trace, pour une exécution : | |
| 8 | + | |
| 9 | + ```bash | |
| 10 | + AGENT_TRACE=true mm agent.yaml | |
| 11 | + ``` | |
| 12 | + | |
| 13 | + ou durablement, dans la configuration : | |
| 14 | + | |
| 15 | + ```yaml | |
| 16 | + trace: | |
| 17 | + enabled: true | |
| 18 | + dir: .mm/traces # la valeur par défaut | |
| 19 | + ``` | |
| 20 | + | |
| 21 | + La bannière se termine par `| trace: /chemin/absolu/.mm/traces` tant qu'elle est active. | |
| 22 | + | |
| 23 | +2. Posez une question comme d'habitude. Chaque appel au modèle laisse une paire de fichiers : | |
| 24 | + | |
| 25 | + ```text | |
| 26 | + .mm/traces/20261007-123237.903-bob-0001.request.json | |
| 27 | + .mm/traces/20261007-123237.903-bob-0001.response-200.sse | |
| 28 | + ``` | |
| 29 | + | |
| 30 | + Une question peut faire plusieurs appels : un par aller-retour avec les outils, plus un résumé de compaction. L'`acp.name` de l'agent figure dans chaque nom, donc un agent appelé par `ask_agent` ou `/ask` — il hérite de `AGENT_TRACE` — écrit ses propres fichiers à côté des vôtres (`…-jan-0001…`). | |
| 31 | + | |
| 32 | +3. Lisez la requête. C'est le corps exactement tel qu'envoyé, indenté : | |
| 33 | + | |
| 34 | + ```bash | |
| 35 | + jq '{model, temperature, stream_options, chat_template_kwargs}' .mm/traces/*-0001.request.json | |
| 36 | + jq -r '.messages[] | "\(.role): \(.content | tostring | .[0:120])"' .mm/traces/*-0001.request.json | |
| 37 | + jq -r '.tools[].function.name' .mm/traces/*-0001.request.json | |
| 38 | + ``` | |
| 39 | + | |
| 40 | +4. Lisez la réponse. Une réponse en streaming (`.sse`) est faite des lignes du serveur telles qu'elles sont arrivées, un chunk `data: {…}` chacune ; reconstituez le texte de la réponse avec : | |
| 41 | + | |
| 42 | + ```bash | |
| 43 | + grep '^data: {' .mm/traces/*-0001.response-200.sse | sed 's/^data: //' \ | |
| 44 | + | jq -rj '.choices[0].delta.content // empty'; echo | |
| 45 | + ``` | |
| 46 | + | |
| 47 | + son raisonnement avec `.choices[0].delta.reasoning_content`, ses appels d'outils avec `.choices[0].delta.tool_calls`, ses comptes de tokens avec `.usage` (le dernier chunk). Une réponse sans streaming est un fichier `.json`, indenté. Un statut d'erreur figure dans le nom (`.response-400.json`) ; un appel resté sans réponse laisse `.response-error.txt`. | |
| 48 | + | |
| 49 | +5. Désactivez-la quand vous avez fini : les fichiers contiennent vos prompts, le contenu des fichiers lus et les sorties des commandes, en entier. `.mm/` est ignoré par git dans ce dépôt ; ajoutez-le aussi au `.gitignore` de votre projet. | |
| 50 | + | |
| 51 | +## Variantes | |
| 52 | + | |
| 53 | +- **Dans un éditeur (ACP).** Un `trace.dir` relatif est dans le `cwd` de la session — le projet ouvert par l'éditeur — et non dans le répertoire d'où l'éditeur a lancé `mm`. Mettez `AGENT_TRACE` dans les réglages de l'agent de l'éditeur (`env`), ou `trace.enabled` dans le fichier : l'éditeur ne voit pas les variables de votre shell. | |
| 54 | +- **Ailleurs.** `trace.dir: /tmp/mm-traces` rassemble les appels de tous les projets au même endroit. | |
| 55 | +- **Seulement les agents interrogés.** Mettez `trace.enabled: true` dans le fichier de cet agent au lieu d'utiliser `AGENT_TRACE`. | |
| 56 | +- **Ce que mm ajoute.** La requête porte déjà ce que mm ajoute lui-même — `stream_options.include_usage`, `return_progress` pour un serveur llama.cpp, `chat_template_kwargs.enable_thinking: false` sur un résumé — la trace sert donc aussi à vérifier ces ajouts. | |
| 57 | + | |
| 58 | +## Voir aussi | |
| 59 | + | |
| 60 | +- Les clés `trace` et les noms des fichiers : [référence de la configuration](../reference/configuration.md) | |
| 61 | +- `AGENT_TRACE` : [référence de la ligne de commande](../reference/cli.md) | |
| 62 | +- Pourquoi le raisonnement arrive dans son propre champ : [Modèles à raisonnement](../explanation/thinking-models.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | +# Comment voir le JSON envoyé au modèle et sa réponse | ||
| 2 | + | ||
| 3 | +Ce guide montre comment garder, pour chaque appel au modèle, la requête exacte envoyée par mm et la réponse exacte renvoyée par le serveur — pour vérifier un prompt système, les définitions d'outils, une clé d'échantillonnage, ce qu'un autre agent a reçu, ou ce qu'un modèle a vraiment répondu. Il suppose que vous lancez déjà `mm` avec un fichier de configuration. | ||
| 4 | + | ||
| 5 | +## Étapes | ||
| 6 | + | ||
| 7 | +1. Activez la trace, pour une exécution : | ||
| 8 | + | ||
| 9 | + ```bash | ||
| 10 | + AGENT_TRACE=true mm agent.yaml | ||
| 11 | + ``` | ||
| 12 | + | ||
| 13 | + ou durablement, dans la configuration : | ||
| 14 | + | ||
| 15 | + ```yaml | ||
| 16 | + trace: | ||
| 17 | + enabled: true | ||
| 18 | + dir: .mm/traces # la valeur par défaut | ||
| 19 | + ``` | ||
| 20 | + | ||
| 21 | + La bannière se termine par `| trace: /chemin/absolu/.mm/traces` tant qu'elle est active. | ||
| 22 | + | ||
| 23 | +2. Posez une question comme d'habitude. Chaque appel au modèle laisse une paire de fichiers : | ||
| 24 | + | ||
| 25 | + ```text | ||
| 26 | + .mm/traces/20261007-123237.903-bob-0001.request.json | ||
| 27 | + .mm/traces/20261007-123237.903-bob-0001.response-200.sse | ||
| 28 | + ``` | ||
| 29 | + | ||
| 30 | + Une question peut faire plusieurs appels : un par aller-retour avec les outils, plus un résumé de compaction. L'`acp.name` de l'agent figure dans chaque nom, donc un agent appelé par `ask_agent` ou `/ask` — il hérite de `AGENT_TRACE` — écrit ses propres fichiers à côté des vôtres (`…-jan-0001…`). | ||
| 31 | + | ||
| 32 | +3. Lisez la requête. C'est le corps exactement tel qu'envoyé, indenté : | ||
| 33 | + | ||
| 34 | + ```bash | ||
| 35 | + jq '{model, temperature, stream_options, chat_template_kwargs}' .mm/traces/*-0001.request.json | ||
| 36 | + jq -r '.messages[] | "\(.role): \(.content | tostring | .[0:120])"' .mm/traces/*-0001.request.json | ||
| 37 | + jq -r '.tools[].function.name' .mm/traces/*-0001.request.json | ||
| 38 | + ``` | ||
| 39 | + | ||
| 40 | +4. Lisez la réponse. Une réponse en streaming (`.sse`) est faite des lignes du serveur telles qu'elles sont arrivées, un chunk `data: {…}` chacune ; reconstituez le texte de la réponse avec : | ||
| 41 | + | ||
| 42 | + ```bash | ||
| 43 | + grep '^data: {' .mm/traces/*-0001.response-200.sse | sed 's/^data: //' \ | ||
| 44 | + | jq -rj '.choices[0].delta.content // empty'; echo | ||
| 45 | + ``` | ||
| 46 | + | ||
| 47 | + son raisonnement avec `.choices[0].delta.reasoning_content`, ses appels d'outils avec `.choices[0].delta.tool_calls`, ses comptes de tokens avec `.usage` (le dernier chunk). Une réponse sans streaming est un fichier `.json`, indenté. Un statut d'erreur figure dans le nom (`.response-400.json`) ; un appel resté sans réponse laisse `.response-error.txt`. | ||
| 48 | + | ||
| 49 | +5. Désactivez-la quand vous avez fini : les fichiers contiennent vos prompts, le contenu des fichiers lus et les sorties des commandes, en entier. `.mm/` est ignoré par git dans ce dépôt ; ajoutez-le aussi au `.gitignore` de votre projet. | ||
| 50 | + | ||
| 51 | +## Variantes | ||
| 52 | + | ||
| 53 | +- **Dans un éditeur (ACP).** Un `trace.dir` relatif est dans le `cwd` de la session — le projet ouvert par l'éditeur — et non dans le répertoire d'où l'éditeur a lancé `mm`. Mettez `AGENT_TRACE` dans les réglages de l'agent de l'éditeur (`env`), ou `trace.enabled` dans le fichier : l'éditeur ne voit pas les variables de votre shell. | ||
| 54 | +- **Ailleurs.** `trace.dir: /tmp/mm-traces` rassemble les appels de tous les projets au même endroit. | ||
| 55 | +- **Seulement les agents interrogés.** Mettez `trace.enabled: true` dans le fichier de cet agent au lieu d'utiliser `AGENT_TRACE`. | ||
| 56 | +- **Ce que mm ajoute.** La requête porte déjà ce que mm ajoute lui-même — `stream_options.include_usage`, `return_progress` pour un serveur llama.cpp, `chat_template_kwargs.enable_thinking: false` sur un résumé — la trace sert donc aussi à vérifier ces ajouts. | ||
| 57 | + | ||
| 58 | +## Voir aussi | ||
| 59 | + | ||
| 60 | +- Les clés `trace` et les noms des fichiers : [référence de la configuration](../reference/configuration.md) | ||
| 61 | +- `AGENT_TRACE` : [référence de la ligne de commande](../reference/cli.md) | ||
| 62 | +- Pourquoi le raisonnement arrive dans son propre champ : [Modèles à raisonnement](../explanation/thinking-models.md) | ||
modified
docs/fr/how-to/use-from-an-editor.md +1 -0 | @@ -32,6 +32,7 @@ Ce guide montre comment héberger `mm` dans un éditeur qui parle l'Agent Client | ||
| 32 | 32 | - **Compresser ou recommencer la conversation.** Envoyez `/compact` ou `/new` ; les éditeurs qui listent les commandes de l'agent proposent les deux dans le menu des commandes slash. Voir la [référence des commandes slash](../reference/slash-commands.md#mode-acp). |
| 33 | 33 | - **Attacher un fichier avec `@`.** Le sélecteur de l'éditeur envoie un `resource_link`, transmis au modèle sous la forme `[attached file: <chemin>]` ; un chemin tapé à la main comme `@chemin` est compris de la même façon (voir [`@chemin`](attach-a-file.md)). Le modèle lit le fichier avec ses outils. |
| 34 | 34 | - **Recharger l'agent sans perdre le fil.** Avec `sessions.enabled: true` dans le fichier de configuration, le Reload Agent de l'éditeur (Zed) appelle `session/load` et toute la conversation, appels d'outils compris, est rejouée dans le fil. Voir [Conserver les sessions et les reprendre](keep-sessions.md). |
| 35 | +- **Voir le JSON échangé avec le modèle.** Activez la trace, dans le fichier de configuration (`trace: {enabled: true}`) ou dans les réglages de l'éditeur (`"env": { "AGENT_CONFIG": "…", "AGENT_TRACE": "true" }` — l'éditeur ne transmet pas les variables de votre shell). Les fichiers arrivent dans `<projet>/.mm/traces/`, le projet étant le `cwd` de la session ; la bannière, dans les logs ACP, indique `trace: <session cwd>/.mm/traces`. Voir [Comment voir le JSON envoyé au modèle](inspect-model-traffic.md). | |
| 35 | 36 | - **Changer le nom que voit l'éditeur.** Ajoutez un bloc `acp:` au fichier de configuration ; `name` est l'identifiant que l'éditeur journalise, `title` le nom qu'il peut afficher (laissez `title` vide pour n'en envoyer aucun). La version envoyée est toujours celle du binaire, celle qu'affiche `mm --version` — vérifiez-la avec la requête `initialize` ci-dessous. Voir les [clés de `acp`](../reference/configuration.md#clés-de-acp). |
| 36 | 37 | |
| 37 | 38 | ```yaml |
| @@ -32,6 +32,7 @@ Ce guide montre comment héberger `mm` dans un éditeur qui parle l'Agent Client | |||
| 32 | - **Compresser ou recommencer la conversation.** Envoyez `/compact` ou `/new` ; les éditeurs qui listent les commandes de l'agent proposent les deux dans le menu des commandes slash. Voir la [référence des commandes slash](../reference/slash-commands.md#mode-acp). | 32 | - **Compresser ou recommencer la conversation.** Envoyez `/compact` ou `/new` ; les éditeurs qui listent les commandes de l'agent proposent les deux dans le menu des commandes slash. Voir la [référence des commandes slash](../reference/slash-commands.md#mode-acp). |
| 33 | - **Attacher un fichier avec `@`.** Le sélecteur de l'éditeur envoie un `resource_link`, transmis au modèle sous la forme `[attached file: <chemin>]` ; un chemin tapé à la main comme `@chemin` est compris de la même façon (voir [`@chemin`](attach-a-file.md)). Le modèle lit le fichier avec ses outils. | 33 | - **Attacher un fichier avec `@`.** Le sélecteur de l'éditeur envoie un `resource_link`, transmis au modèle sous la forme `[attached file: <chemin>]` ; un chemin tapé à la main comme `@chemin` est compris de la même façon (voir [`@chemin`](attach-a-file.md)). Le modèle lit le fichier avec ses outils. |
| 34 | - **Recharger l'agent sans perdre le fil.** Avec `sessions.enabled: true` dans le fichier de configuration, le Reload Agent de l'éditeur (Zed) appelle `session/load` et toute la conversation, appels d'outils compris, est rejouée dans le fil. Voir [Conserver les sessions et les reprendre](keep-sessions.md). | 34 | - **Recharger l'agent sans perdre le fil.** Avec `sessions.enabled: true` dans le fichier de configuration, le Reload Agent de l'éditeur (Zed) appelle `session/load` et toute la conversation, appels d'outils compris, est rejouée dans le fil. Voir [Conserver les sessions et les reprendre](keep-sessions.md). |
| 35 | +- **Voir le JSON échangé avec le modèle.** Activez la trace, dans le fichier de configuration (`trace: {enabled: true}`) ou dans les réglages de l'éditeur (`"env": { "AGENT_CONFIG": "…", "AGENT_TRACE": "true" }` — l'éditeur ne transmet pas les variables de votre shell). Les fichiers arrivent dans `<projet>/.mm/traces/`, le projet étant le `cwd` de la session ; la bannière, dans les logs ACP, indique `trace: <session cwd>/.mm/traces`. Voir [Comment voir le JSON envoyé au modèle](inspect-model-traffic.md). | ||
| 35 | - **Changer le nom que voit l'éditeur.** Ajoutez un bloc `acp:` au fichier de configuration ; `name` est l'identifiant que l'éditeur journalise, `title` le nom qu'il peut afficher (laissez `title` vide pour n'en envoyer aucun). La version envoyée est toujours celle du binaire, celle qu'affiche `mm --version` — vérifiez-la avec la requête `initialize` ci-dessous. Voir les [clés de `acp`](../reference/configuration.md#clés-de-acp). | 36 | - **Changer le nom que voit l'éditeur.** Ajoutez un bloc `acp:` au fichier de configuration ; `name` est l'identifiant que l'éditeur journalise, `title` le nom qu'il peut afficher (laissez `title` vide pour n'en envoyer aucun). La version envoyée est toujours celle du binaire, celle qu'affiche `mm --version` — vérifiez-la avec la requête `initialize` ci-dessous. Voir les [clés de `acp`](../reference/configuration.md#clés-de-acp). |
| 36 | 37 | ||
| 37 | ```yaml | 38 | ```yaml |
modified
docs/fr/how-to/use-the-window-mode.md +1 -0 | @@ -28,6 +28,7 @@ Ce guide montre comment lancer mini-me dans une fenêtre unique en plein écran | ||
| 28 | 28 | |
| 29 | 29 | - **Demander avant chaque `bash` et chaque outil de fichier.** Mettez `yolo: false` dans le YAML. La zone de saisie est remplacée par la question `? bash: rm -rf build` et quatre touches y répondent : `a` autoriser une fois, `s` autoriser cet outil pour la session, `r` (ou Échap) refuser, `n` ne jamais autoriser cet outil dans cette session. Un outil refusé dit au modèle `[command rejected by the user]`. La valeur par défaut, `yolo: true`, exécute les outils sans demander. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). |
| 30 | 30 | - **Désigner un fichier au modèle.** `@chemin` fonctionne exactement comme dans le mode ligne : les chemins existants sont attachés et repris en lignes `📎`. Voir [Désigner un fichier au modèle avec `@chemin`](attach-a-file.md). |
| 31 | +- **Voir quel agent répond.** L'en-tête et l'étiquette au-dessus de chaque réponse nomment l'agent : `mm`, ou `mm [<acp.title>]` quand la configuration donne un titre. Pendant qu'un autre agent travaille — `/ask-<nom>`, ou l'outil `ask_agent` — l'en-tête le nomme avec son modèle et `asked by` votre agent, et sa réponse porte son titre (ou son nom dans `mmConfigs`) ; puis les deux reviennent à votre agent. Voir [Laisser votre agent appeler d'autres agents](let-the-agent-call-other-agents.md). | |
| 31 | 32 | - **Conserver et reprendre des sessions.** Avec `sessions.enabled: true`, l'en-tête affiche l'identifiant de la session enregistrée ; `/sessions` et `/resume` fonctionnent comme dans le mode ligne. Voir [Conserver les sessions et les reprendre](keep-sessions.md). |
| 32 | 33 | - **Faire défiler la conversation.** PgUp/PgDn paginent ; **Tab** donne le focus à la conversation pour que ↑/↓, Début et Fin la fassent défiler ; Fin reprend le suivi de la fin. **Ctrl-L** efface la conversation affichée sans toucher à l'historique. La molette ne fait défiler qu'après **F2** (voir plus bas). |
| 33 | 34 | - **Pas de terminal.** `mm -tui | tee log.txt`, ou tout lancement où stdin ou stdout n'est pas un terminal, affiche `[tui: stdin or stdout is not a terminal — running the line mode]` et lance le mode ligne. Rien d'autre ne change. |
| @@ -28,6 +28,7 @@ Ce guide montre comment lancer mini-me dans une fenêtre unique en plein écran | |||
| 28 | 28 | ||
| 29 | - **Demander avant chaque `bash` et chaque outil de fichier.** Mettez `yolo: false` dans le YAML. La zone de saisie est remplacée par la question `? bash: rm -rf build` et quatre touches y répondent : `a` autoriser une fois, `s` autoriser cet outil pour la session, `r` (ou Échap) refuser, `n` ne jamais autoriser cet outil dans cette session. Un outil refusé dit au modèle `[command rejected by the user]`. La valeur par défaut, `yolo: true`, exécute les outils sans demander. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). | 29 | - **Demander avant chaque `bash` et chaque outil de fichier.** Mettez `yolo: false` dans le YAML. La zone de saisie est remplacée par la question `? bash: rm -rf build` et quatre touches y répondent : `a` autoriser une fois, `s` autoriser cet outil pour la session, `r` (ou Échap) refuser, `n` ne jamais autoriser cet outil dans cette session. Un outil refusé dit au modèle `[command rejected by the user]`. La valeur par défaut, `yolo: true`, exécute les outils sans demander. Voir [Confirmer les appels d'outils](confirm-tool-calls.md). |
| 30 | - **Désigner un fichier au modèle.** `@chemin` fonctionne exactement comme dans le mode ligne : les chemins existants sont attachés et repris en lignes `📎`. Voir [Désigner un fichier au modèle avec `@chemin`](attach-a-file.md). | 30 | - **Désigner un fichier au modèle.** `@chemin` fonctionne exactement comme dans le mode ligne : les chemins existants sont attachés et repris en lignes `📎`. Voir [Désigner un fichier au modèle avec `@chemin`](attach-a-file.md). |
| 31 | +- **Voir quel agent répond.** L'en-tête et l'étiquette au-dessus de chaque réponse nomment l'agent : `mm`, ou `mm [<acp.title>]` quand la configuration donne un titre. Pendant qu'un autre agent travaille — `/ask-<nom>`, ou l'outil `ask_agent` — l'en-tête le nomme avec son modèle et `asked by` votre agent, et sa réponse porte son titre (ou son nom dans `mmConfigs`) ; puis les deux reviennent à votre agent. Voir [Laisser votre agent appeler d'autres agents](let-the-agent-call-other-agents.md). | ||
| 31 | - **Conserver et reprendre des sessions.** Avec `sessions.enabled: true`, l'en-tête affiche l'identifiant de la session enregistrée ; `/sessions` et `/resume` fonctionnent comme dans le mode ligne. Voir [Conserver les sessions et les reprendre](keep-sessions.md). | 32 | - **Conserver et reprendre des sessions.** Avec `sessions.enabled: true`, l'en-tête affiche l'identifiant de la session enregistrée ; `/sessions` et `/resume` fonctionnent comme dans le mode ligne. Voir [Conserver les sessions et les reprendre](keep-sessions.md). |
| 32 | - **Faire défiler la conversation.** PgUp/PgDn paginent ; **Tab** donne le focus à la conversation pour que ↑/↓, Début et Fin la fassent défiler ; Fin reprend le suivi de la fin. **Ctrl-L** efface la conversation affichée sans toucher à l'historique. La molette ne fait défiler qu'après **F2** (voir plus bas). | 33 | - **Faire défiler la conversation.** PgUp/PgDn paginent ; **Tab** donne le focus à la conversation pour que ↑/↓, Début et Fin la fassent défiler ; Fin reprend le suivi de la fin. **Ctrl-L** efface la conversation affichée sans toucher à l'historique. La molette ne fait défiler qu'après **F2** (voir plus bas). |
| 33 | - **Pas de terminal.** `mm -tui | tee log.txt`, ou tout lancement où stdin ou stdout n'est pas un terminal, affiche `[tui: stdin or stdout is not a terminal — running the line mode]` et lance le mode ligne. Rien d'autre ne change. | 34 | - **Pas de terminal.** `mm -tui | tee log.txt`, ou tout lancement où stdin ou stdout n'est pas un terminal, affiche `[tui: stdin or stdout is not a terminal — running the line mode]` et lance le mode ligne. Rien d'autre ne change. |
modified
docs/fr/reference/cli.md +4 -3 | @@ -60,7 +60,8 @@ La première source existante gagne. | ||
| 60 | 60 | | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. | |
| 61 | 61 | | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. | |
| 62 | 62 | | `AGENT_SESSIONS` | `off` désactive l'enregistrement des sessions pour cette exécution, quoi que dise `sessions.enabled`. Posée par `/ask` sur les agents qu'il lance. | |
| 63 | -| `AGENT_YOLO` | Surcharge la clé `yolo` : `true` ou `false` (toute graphie acceptée par `strconv.ParseBool` ; toute autre valeur est une erreur de configuration). Posée à `false` par `/ask` sur les agents qu'il lance, pour que l'agent qui interroge décide. | | |
| 63 | +| `AGENT_TRACE` | Surcharge `trace.enabled` : `true` ou `false` (toute graphie acceptée par `strconv.ParseBool` ; toute autre valeur est une erreur de configuration). Héritée par les agents que lancent `/ask` et `ask_agent`. | | |
| 64 | +| `AGENT_YOLO` | Surcharge la clé `yolo` : `true` ou `false` (toute graphie acceptée par `strconv.ParseBool` ; toute autre valeur est une erreur de configuration). Posée à `false` par `/ask` et `ask_agent` sur les agents qu'ils lancent, pour que le côté qui interroge décide (le `yolo` de l'entrée `mmConfigs` de l'agent, sinon celui de l'agent qui interroge). | | |
| 64 | 65 | | `MM_BIN` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin du binaire `mm` en cours. | |
| 65 | 66 | | `MM_CONFIG` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. | |
| 66 | 67 | | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. | |
| @@ -73,7 +74,7 @@ Affichée sur stdout en mode terminal, sur stderr en mode ACP. Grisée quand std | ||
| 73 | 74 | | Ligne | Contenu | |
| 74 | 75 | |-------|---------| |
| 75 | 76 | | `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), la clé obsolète `permissions`. | |
| 76 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`. | | |
| 77 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| trace: …]` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`., et, tant que `trace.enabled` est actif, `trace: <répertoire absolu>` (`trace: <session cwd>/<dir>` en mode ACP). | | |
| 77 | 78 | | `Agent ready. Commands: …` | Mode terminal seulement. | |
| 78 | 79 | | `Shortcut: Ctrl+C …` | Mode terminal seulement. | |
| 79 | 80 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Mode terminal, avec `sessions.enabled` seulement : le répertoire de sessions résolu et le nombre de sessions qu'il contient déjà. | |
| @@ -141,7 +142,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 141 | 142 | | Contexte (`--ask-with-context`) | stdin. Sans `-p`, stdin contient `<contexte>`, une ligne `---`, puis la question. | |
| 142 | 143 | | stdout | La réponse de l'agent, et rien d'autre, suivie d'un saut de ligne. | |
| 143 | 144 | | stderr | `🗣 asking <agent> (<fournisseur>/<modèle>)…`, une ligne `🛠️ <agent> · <outil>: <arguments>` par appel d'outil, `[ask: …]` en cas d'erreur. | |
| 144 | -| Appels d'outils de l'agent | Décidés par le `yolo` de cette configuration : `true` = autorisés ; `false` = refusés, avec la même ligne sur stderr qu'en mode invite (stdin portait la question, personne ne peut répondre). | | |
| 145 | +| Appels d'outils de l'agent | Décidés par le `yolo` de l'entrée `mmConfigs` de l'agent s'il est fixé, sinon par le `yolo` de cette configuration : `true` = autorisés ; `false` = refusés, avec la même ligne sur stderr qu'en mode invite (stdin portait la question, personne ne peut répondre). | | |
| 145 | 146 | | Processus parent | Surveillé toutes les 500 ms : s'il disparaît, la demande est annulée et l'agent arrêté. | |
| 146 | 147 | | Session | Aucune : ni ce processus ni l'agent n'écrivent de fichier de session. | |
| 147 | 148 | |
| @@ -60,7 +60,8 @@ La première source existante gagne. | |||
| 60 | | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. | | 60 | | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. | |
| 61 | | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. | | 61 | | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. | |
| 62 | | `AGENT_SESSIONS` | `off` désactive l'enregistrement des sessions pour cette exécution, quoi que dise `sessions.enabled`. Posée par `/ask` sur les agents qu'il lance. | | 62 | | `AGENT_SESSIONS` | `off` désactive l'enregistrement des sessions pour cette exécution, quoi que dise `sessions.enabled`. Posée par `/ask` sur les agents qu'il lance. | |
| 63 | -| `AGENT_YOLO` | Surcharge la clé `yolo` : `true` ou `false` (toute graphie acceptée par `strconv.ParseBool` ; toute autre valeur est une erreur de configuration). Posée à `false` par `/ask` sur les agents qu'il lance, pour que l'agent qui interroge décide. | | 63 | +| `AGENT_TRACE` | Surcharge `trace.enabled` : `true` ou `false` (toute graphie acceptée par `strconv.ParseBool` ; toute autre valeur est une erreur de configuration). Héritée par les agents que lancent `/ask` et `ask_agent`. | |
| 64 | +| `AGENT_YOLO` | Surcharge la clé `yolo` : `true` ou `false` (toute graphie acceptée par `strconv.ParseBool` ; toute autre valeur est une erreur de configuration). Posée à `false` par `/ask` et `ask_agent` sur les agents qu'ils lancent, pour que le côté qui interroge décide (le `yolo` de l'entrée `mmConfigs` de l'agent, sinon celui de l'agent qui interroge). | | ||
| 64 | | `MM_BIN` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin du binaire `mm` en cours. | | 65 | | `MM_BIN` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin du binaire `mm` en cours. | |
| 65 | | `MM_CONFIG` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. | | 66 | | `MM_CONFIG` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. | |
| 66 | | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. | | 67 | | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. | |
| @@ -73,7 +74,7 @@ Affichée sur stdout en mode terminal, sur stderr en mode ACP. Grisée quand std | |||
| 73 | | Ligne | Contenu | | 74 | | Ligne | Contenu | |
| 74 | |-------|---------| | 75 | |-------|---------| |
| 75 | | `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), la clé obsolète `permissions`. | | 76 | | `[warning: …]` | Zéro ou plusieurs avertissements de la sonde : serveur injoignable, modèle absent de la liste du serveur, pas de template de chat, compression qui ne peut jamais se déclencher, aucun outil déclaré, un fichier de `mmConfigs` introuvable (`mmConfigs: <name> → <path> not found — /ask-<name> will fail`), la clé obsolète `permissions`. | |
| 76 | -| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`. | | 77 | +| `mm <version> \| config: … \| provider: … \| model: … \| ctx: … \| tools: … \| skills: N \| yolo: …[ \| trace: …]` | Version (voir ci-dessus), chemin de la configuration, nom du fournisseur, identifiant du modèle, fenêtre de contexte avec son origine (`config`, `/props`) ou `unknown`, noms des outils déclarés, nombre de skills trouvés, `yolo: on` ou `yolo: off (tools ask)`., et, tant que `trace.enabled` est actif, `trace: <répertoire absolu>` (`trace: <session cwd>/<dir>` en mode ACP). | |
| 77 | | `Agent ready. Commands: …` | Mode terminal seulement. | | 78 | | `Agent ready. Commands: …` | Mode terminal seulement. | |
| 78 | | `Shortcut: Ctrl+C …` | Mode terminal seulement. | | 79 | | `Shortcut: Ctrl+C …` | Mode terminal seulement. | |
| 79 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Mode terminal, avec `sessions.enabled` seulement : le répertoire de sessions résolu et le nombre de sessions qu'il contient déjà. | | 80 | | `sessions: saved under <dir>[ · N earlier — /resume to continue the latest, /sessions to list]` | Mode terminal, avec `sessions.enabled` seulement : le répertoire de sessions résolu et le nombre de sessions qu'il contient déjà. | |
| @@ -141,7 +142,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 141 | | Contexte (`--ask-with-context`) | stdin. Sans `-p`, stdin contient `<contexte>`, une ligne `---`, puis la question. | | 142 | | Contexte (`--ask-with-context`) | stdin. Sans `-p`, stdin contient `<contexte>`, une ligne `---`, puis la question. | |
| 142 | | stdout | La réponse de l'agent, et rien d'autre, suivie d'un saut de ligne. | | 143 | | stdout | La réponse de l'agent, et rien d'autre, suivie d'un saut de ligne. | |
| 143 | | stderr | `🗣 asking <agent> (<fournisseur>/<modèle>)…`, une ligne `🛠️ <agent> · <outil>: <arguments>` par appel d'outil, `[ask: …]` en cas d'erreur. | | 144 | | stderr | `🗣 asking <agent> (<fournisseur>/<modèle>)…`, une ligne `🛠️ <agent> · <outil>: <arguments>` par appel d'outil, `[ask: …]` en cas d'erreur. | |
| 144 | -| Appels d'outils de l'agent | Décidés par le `yolo` de cette configuration : `true` = autorisés ; `false` = refusés, avec la même ligne sur stderr qu'en mode invite (stdin portait la question, personne ne peut répondre). | | 145 | +| Appels d'outils de l'agent | Décidés par le `yolo` de l'entrée `mmConfigs` de l'agent s'il est fixé, sinon par le `yolo` de cette configuration : `true` = autorisés ; `false` = refusés, avec la même ligne sur stderr qu'en mode invite (stdin portait la question, personne ne peut répondre). | |
| 145 | | Processus parent | Surveillé toutes les 500 ms : s'il disparaît, la demande est annulée et l'agent arrêté. | | 146 | | Processus parent | Surveillé toutes les 500 ms : s'il disparaît, la demande est annulée et l'agent arrêté. | |
| 146 | | Session | Aucune : ni ce processus ni l'agent n'écrivent de fichier de session. | | 147 | | Session | Aucune : ni ce processus ni l'agent n'écrivent de fichier de session. | |
| 147 | 148 | ||
modified
docs/fr/reference/configuration.md +12 -1 | @@ -28,10 +28,11 @@ | ||
| 28 | 28 | | `bashTimeout` | durée | `30s` | Durée maximale d'une commande `bash` avant qu'elle soit tuée ; l'outil répond alors `[error: <bashTimeout> timeout exceeded]`. Doit être > 0. Le watchdog continue de compter pendant qu'une commande tourne (ticket 57) : une commande plus longue que `watchdogTimeout` termine le tour, donc relevez les deux ensemble. | |
| 29 | 29 | | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. | |
| 30 | 30 | | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. | |
| 31 | +| `trace` | dictionnaire | voir ci-dessous | Écrit chaque requête au modèle et chaque réponse dans des fichiers. | | |
| 31 | 32 | | `acp` | dictionnaire | voir ci-dessous | La façon dont `mm -acp` se présente à l'éditeur. | |
| 32 | 33 | | `lsp` | dictionnaire | voir ci-dessous | Serveurs de langage derrière l'outil `lsp`. Aucun serveur listé = pas d'outil. | |
| 33 | 34 | | `tui` | bool | `false` | Démarre dans le mode fenêtre unique sans l'option `-tui`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal ; ignoré avec `-acp`. | |
| 34 | -| `yolo` | booléen | `true` | `true` : les outils s'exécutent sans demander. `false` : chaque appel qui modifie quelque chose — `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp`, `create`/`update`/`next`/`delete` de `plan` — attend la réponse de l'utilisateur dans toutes les façades (mode ligne, fenêtre, éditeur), de même que chaque outil MCP, quoi que dise `mcp.permissions`. Réponses : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, jamais (cet outil, pour la session). `read_file`, `read_skill`, `fetch_content` sans `output` et les actions de navigation de `lsp` ne demandent jamais. Avec `-p` et `mm --ask`, personne ne peut répondre : ces appels sont refusés. Un agent lancé par `/ask` tourne avec `AGENT_YOLO=false` et soumet ses appels à l'agent qui l'interroge, dont le `yolo` décide. Surchargée par `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` décident toujours quels outils existent. Voir [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). | | |
| 35 | +| `yolo` | booléen | `true` | `true` : les outils s'exécutent sans demander. `false` : chaque appel qui modifie quelque chose — `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp`, `create`/`update`/`next`/`delete` de `plan` — attend la réponse de l'utilisateur dans toutes les façades (mode ligne, fenêtre, éditeur), de même que chaque outil MCP, quoi que dise `mcp.permissions`. Réponses : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, jamais (cet outil, pour la session). `read_file`, `read_skill`, `fetch_content` sans `output` et les actions de navigation de `lsp` ne demandent jamais. Avec `-p` et `mm --ask`, personne ne peut répondre : ces appels sont refusés. Un agent lancé par `/ask`, `ask_agent` ou `mm --ask` tourne avec `AGENT_YOLO=false` et soumet ses appels à l'agent qui l'interroge : le `yolo` de son entrée `mmConfigs` décide s'il est fixé, sinon le `yolo` de l'agent qui interroge. Surchargée par `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` décident toujours quels outils existent. Voir [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). | | |
| 35 | 36 | | `permissions` | chaîne | absente | Obsolète, remplacée par `yolo`. `ask` = `yolo: false` (et l'emporte sur `yolo: true`) ; `auto` ne change rien. Les deux valeurs ajoutent un avertissement au démarrage. Doit valoir `auto` ou `ask`. | |
| 36 | 37 | | `mmConfigs` | dictionnaire nom → chemin, ou nom → `{path, description, yolo}` | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)), et la liste déclare l'outil [`ask_agent`](tools.md#ask_agent), par lequel le modèle les appelle lui-même. La forme longue ajoute `description` (quand appeler cet agent ; affichée dans la description d'`ask_agent`, à défaut l'`acp.title` du fichier de l'agent) et `yolo` (décide des appels d'outils de cet agent quand il est interrogé, par `/ask`, `ask_agent` ou `mm --ask`, à la place du `yolo` de cet agent-ci : `true` ils s'exécutent, `false` chacun est demandé ; absent, le `yolo` de cet agent-ci décide). `path` ne doit pas être vide. Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. | |
| 37 | 38 | |
| @@ -54,6 +55,15 @@ | ||
| 54 | 55 | | `sessions.enabled` | bool | `false` | Enregistre chaque conversation dans un fichier, réécrit après chaque tour, `/compact` et `/new`. Le fichier enregistre aussi, hors de l'historique, chaque erreur affichée (tableau `errors` : génération échouée, compression échouée, `/resume` échoué). Active `/sessions` et `/resume` dans le terminal et `session/load` en ACP. | |
| 55 | 56 | | `sessions.dir` | chaîne | `.mm/sessions` | Répertoire des fichiers de session, un `<id>.json` chacun. Relatif au répertoire de lancement de `mm` (terminal) ou au `cwd` de la session (ACP) ; un chemin absolu est utilisé tel quel. Ne doit pas être vide. | |
| 56 | 57 | |
| 58 | +## Clés de `trace` | |
| 59 | + | |
| 60 | +| Clé | Type | Défaut | Description | | |
| 61 | +|-----|------|--------|-------------| | |
| 62 | +| `trace.enabled` | bool | `false` | Écrit une paire de fichiers par appel au modèle : le corps de la requête tel qu'envoyé (après les champs ajoutés par mm : `stream_options`, `return_progress`, l'interrupteur de raisonnement d'un résumé), indenté, et le corps de la réponse tel que reçu. Surchargé par `AGENT_TRACE`. Les en-têtes ne sont jamais écrits. La bannière se termine par ` \| trace: <répertoire absolu>` tant qu'elle est active (` \| trace: <session cwd>/<dir>` en mode ACP). | | |
| 63 | +| `trace.dir` | chaîne | `.mm/traces` | Répertoire des fichiers. Un chemin relatif est résolu à chaque appel dans le répertoire de travail du tour — le `cwd` de la session en mode ACP, comme `sessions.dir` — sinon dans le répertoire où `mm` tourne ; un chemin absolu est utilisé tel quel. Un répertoire impossible à créer affiche une fois `[trace: cannot write to <dir>: <erreur> — nothing is traced]` (sur stderr en mode ACP) ; les appels continuent. Créé au besoin. Ne doit pas être vide quand la trace est active. | | |
| 64 | + | |
| 65 | +Noms des fichiers : `<AAAAMMJJ-HHMMSS.mmm>-<acp.name>-<NNNN>.request.json`, puis `….response-<statut>.sse` (une réponse en streaming, les lignes SSE du serveur octet pour octet, écrites à leur arrivée), `….response-<statut>.json` (un corps JSON entier, indenté), ou `….response-error.txt` (pas de réponse : la méthode, l'URL et l'erreur). `NNNN` compte les appels au modèle du processus ; une nouvelle tentative du client HTTP est un appel à part. Un agent lancé par `/ask` ou `ask_agent` hérite de `AGENT_TRACE` et écrit à côté de celui qui l'interroge, sous son propre `acp.name`. | |
| 66 | + | |
| 57 | 67 | ## Clés de `acp` |
| 58 | 68 | |
| 59 | 69 | | Clé | Type | Défaut | Description | |
| @@ -113,6 +123,7 @@ Signalées sous la forme `[config error: <chemin>: <message>]`, code de sortie 1 | ||
| 113 | 123 | | `context.summaryMaxTokens must be > 0` | zéro ou négatif | |
| 114 | 124 | | `context.maxMessages must be >= 0` | valeur négative | |
| 115 | 125 | | `sessions.dir must not be empty` | chaîne vide | |
| 126 | +| `trace.dir must not be empty` | chaîne vide avec la trace active | | |
| 116 | 127 | | `acp.name must not be empty` | chaîne vide ou blanche | |
| 117 | 128 | | `lsp.servers[N].name must not be empty` | un serveur sans `name` | |
| 118 | 129 | | `lsp.servers[N].command must not be empty` | un serveur sans `command` | |
| @@ -28,10 +28,11 @@ | |||
| 28 | | `bashTimeout` | durée | `30s` | Durée maximale d'une commande `bash` avant qu'elle soit tuée ; l'outil répond alors `[error: <bashTimeout> timeout exceeded]`. Doit être > 0. Le watchdog continue de compter pendant qu'une commande tourne (ticket 57) : une commande plus longue que `watchdogTimeout` termine le tour, donc relevez les deux ensemble. | | 28 | | `bashTimeout` | durée | `30s` | Durée maximale d'une commande `bash` avant qu'elle soit tuée ; l'outil répond alors `[error: <bashTimeout> timeout exceeded]`. Doit être > 0. Le watchdog continue de compter pendant qu'une commande tourne (ticket 57) : une commande plus longue que `watchdogTimeout` termine le tour, donc relevez les deux ensemble. | |
| 29 | | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. | | 29 | | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. | |
| 30 | | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. | | 30 | | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. | |
| 31 | +| `trace` | dictionnaire | voir ci-dessous | Écrit chaque requête au modèle et chaque réponse dans des fichiers. | | ||
| 31 | | `acp` | dictionnaire | voir ci-dessous | La façon dont `mm -acp` se présente à l'éditeur. | | 32 | | `acp` | dictionnaire | voir ci-dessous | La façon dont `mm -acp` se présente à l'éditeur. | |
| 32 | | `lsp` | dictionnaire | voir ci-dessous | Serveurs de langage derrière l'outil `lsp`. Aucun serveur listé = pas d'outil. | | 33 | | `lsp` | dictionnaire | voir ci-dessous | Serveurs de langage derrière l'outil `lsp`. Aucun serveur listé = pas d'outil. | |
| 33 | | `tui` | bool | `false` | Démarre dans le mode fenêtre unique sans l'option `-tui`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal ; ignoré avec `-acp`. | | 34 | | `tui` | bool | `false` | Démarre dans le mode fenêtre unique sans l'option `-tui`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal ; ignoré avec `-acp`. | |
| 34 | -| `yolo` | booléen | `true` | `true` : les outils s'exécutent sans demander. `false` : chaque appel qui modifie quelque chose — `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp`, `create`/`update`/`next`/`delete` de `plan` — attend la réponse de l'utilisateur dans toutes les façades (mode ligne, fenêtre, éditeur), de même que chaque outil MCP, quoi que dise `mcp.permissions`. Réponses : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, jamais (cet outil, pour la session). `read_file`, `read_skill`, `fetch_content` sans `output` et les actions de navigation de `lsp` ne demandent jamais. Avec `-p` et `mm --ask`, personne ne peut répondre : ces appels sont refusés. Un agent lancé par `/ask` tourne avec `AGENT_YOLO=false` et soumet ses appels à l'agent qui l'interroge, dont le `yolo` décide. Surchargée par `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` décident toujours quels outils existent. Voir [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). | | 35 | +| `yolo` | booléen | `true` | `true` : les outils s'exécutent sans demander. `false` : chaque appel qui modifie quelque chose — `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp`, `create`/`update`/`next`/`delete` de `plan` — attend la réponse de l'utilisateur dans toutes les façades (mode ligne, fenêtre, éditeur), de même que chaque outil MCP, quoi que dise `mcp.permissions`. Réponses : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, jamais (cet outil, pour la session). `read_file`, `read_skill`, `fetch_content` sans `output` et les actions de navigation de `lsp` ne demandent jamais. Avec `-p` et `mm --ask`, personne ne peut répondre : ces appels sont refusés. Un agent lancé par `/ask`, `ask_agent` ou `mm --ask` tourne avec `AGENT_YOLO=false` et soumet ses appels à l'agent qui l'interroge : le `yolo` de son entrée `mmConfigs` décide s'il est fixé, sinon le `yolo` de l'agent qui interroge. Surchargée par `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` décident toujours quels outils existent. Voir [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). | |
| 35 | | `permissions` | chaîne | absente | Obsolète, remplacée par `yolo`. `ask` = `yolo: false` (et l'emporte sur `yolo: true`) ; `auto` ne change rien. Les deux valeurs ajoutent un avertissement au démarrage. Doit valoir `auto` ou `ask`. | | 36 | | `permissions` | chaîne | absente | Obsolète, remplacée par `yolo`. `ask` = `yolo: false` (et l'emporte sur `yolo: true`) ; `auto` ne change rien. Les deux valeurs ajoutent un avertissement au démarrage. Doit valoir `auto` ou `ask`. | |
| 36 | | `mmConfigs` | dictionnaire nom → chemin, ou nom → `{path, description, yolo}` | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)), et la liste déclare l'outil [`ask_agent`](tools.md#ask_agent), par lequel le modèle les appelle lui-même. La forme longue ajoute `description` (quand appeler cet agent ; affichée dans la description d'`ask_agent`, à défaut l'`acp.title` du fichier de l'agent) et `yolo` (décide des appels d'outils de cet agent quand il est interrogé, par `/ask`, `ask_agent` ou `mm --ask`, à la place du `yolo` de cet agent-ci : `true` ils s'exécutent, `false` chacun est demandé ; absent, le `yolo` de cet agent-ci décide). `path` ne doit pas être vide. Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. | | 37 | | `mmConfigs` | dictionnaire nom → chemin, ou nom → `{path, description, yolo}` | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)), et la liste déclare l'outil [`ask_agent`](tools.md#ask_agent), par lequel le modèle les appelle lui-même. La forme longue ajoute `description` (quand appeler cet agent ; affichée dans la description d'`ask_agent`, à défaut l'`acp.title` du fichier de l'agent) et `yolo` (décide des appels d'outils de cet agent quand il est interrogé, par `/ask`, `ask_agent` ou `mm --ask`, à la place du `yolo` de cet agent-ci : `true` ils s'exécutent, `false` chacun est demandé ; absent, le `yolo` de cet agent-ci décide). `path` ne doit pas être vide. Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. | |
| 37 | 38 | ||
| @@ -54,6 +55,15 @@ | |||
| 54 | | `sessions.enabled` | bool | `false` | Enregistre chaque conversation dans un fichier, réécrit après chaque tour, `/compact` et `/new`. Le fichier enregistre aussi, hors de l'historique, chaque erreur affichée (tableau `errors` : génération échouée, compression échouée, `/resume` échoué). Active `/sessions` et `/resume` dans le terminal et `session/load` en ACP. | | 55 | | `sessions.enabled` | bool | `false` | Enregistre chaque conversation dans un fichier, réécrit après chaque tour, `/compact` et `/new`. Le fichier enregistre aussi, hors de l'historique, chaque erreur affichée (tableau `errors` : génération échouée, compression échouée, `/resume` échoué). Active `/sessions` et `/resume` dans le terminal et `session/load` en ACP. | |
| 55 | | `sessions.dir` | chaîne | `.mm/sessions` | Répertoire des fichiers de session, un `<id>.json` chacun. Relatif au répertoire de lancement de `mm` (terminal) ou au `cwd` de la session (ACP) ; un chemin absolu est utilisé tel quel. Ne doit pas être vide. | | 56 | | `sessions.dir` | chaîne | `.mm/sessions` | Répertoire des fichiers de session, un `<id>.json` chacun. Relatif au répertoire de lancement de `mm` (terminal) ou au `cwd` de la session (ACP) ; un chemin absolu est utilisé tel quel. Ne doit pas être vide. | |
| 56 | 57 | ||
| 58 | +## Clés de `trace` | ||
| 59 | + | ||
| 60 | +| Clé | Type | Défaut | Description | | ||
| 61 | +|-----|------|--------|-------------| | ||
| 62 | +| `trace.enabled` | bool | `false` | Écrit une paire de fichiers par appel au modèle : le corps de la requête tel qu'envoyé (après les champs ajoutés par mm : `stream_options`, `return_progress`, l'interrupteur de raisonnement d'un résumé), indenté, et le corps de la réponse tel que reçu. Surchargé par `AGENT_TRACE`. Les en-têtes ne sont jamais écrits. La bannière se termine par ` \| trace: <répertoire absolu>` tant qu'elle est active (` \| trace: <session cwd>/<dir>` en mode ACP). | | ||
| 63 | +| `trace.dir` | chaîne | `.mm/traces` | Répertoire des fichiers. Un chemin relatif est résolu à chaque appel dans le répertoire de travail du tour — le `cwd` de la session en mode ACP, comme `sessions.dir` — sinon dans le répertoire où `mm` tourne ; un chemin absolu est utilisé tel quel. Un répertoire impossible à créer affiche une fois `[trace: cannot write to <dir>: <erreur> — nothing is traced]` (sur stderr en mode ACP) ; les appels continuent. Créé au besoin. Ne doit pas être vide quand la trace est active. | | ||
| 64 | + | ||
| 65 | +Noms des fichiers : `<AAAAMMJJ-HHMMSS.mmm>-<acp.name>-<NNNN>.request.json`, puis `….response-<statut>.sse` (une réponse en streaming, les lignes SSE du serveur octet pour octet, écrites à leur arrivée), `….response-<statut>.json` (un corps JSON entier, indenté), ou `….response-error.txt` (pas de réponse : la méthode, l'URL et l'erreur). `NNNN` compte les appels au modèle du processus ; une nouvelle tentative du client HTTP est un appel à part. Un agent lancé par `/ask` ou `ask_agent` hérite de `AGENT_TRACE` et écrit à côté de celui qui l'interroge, sous son propre `acp.name`. | ||
| 66 | + | ||
| 57 | ## Clés de `acp` | 67 | ## Clés de `acp` |
| 58 | 68 | ||
| 59 | | Clé | Type | Défaut | Description | | 69 | | Clé | Type | Défaut | Description | |
| @@ -113,6 +123,7 @@ Signalées sous la forme `[config error: <chemin>: <message>]`, code de sortie 1 | |||
| 113 | | `context.summaryMaxTokens must be > 0` | zéro ou négatif | | 123 | | `context.summaryMaxTokens must be > 0` | zéro ou négatif | |
| 114 | | `context.maxMessages must be >= 0` | valeur négative | | 124 | | `context.maxMessages must be >= 0` | valeur négative | |
| 115 | | `sessions.dir must not be empty` | chaîne vide | | 125 | | `sessions.dir must not be empty` | chaîne vide | |
| 126 | +| `trace.dir must not be empty` | chaîne vide avec la trace active | | ||
| 116 | | `acp.name must not be empty` | chaîne vide ou blanche | | 127 | | `acp.name must not be empty` | chaîne vide ou blanche | |
| 117 | | `lsp.servers[N].name must not be empty` | un serveur sans `name` | | 128 | | `lsp.servers[N].name must not be empty` | un serveur sans `name` | |
| 118 | | `lsp.servers[N].command must not be empty` | un serveur sans `command` | | 129 | | `lsp.servers[N].command must not be empty` | un serveur sans `command` | |
modified
internal/config/config.go +33 -0 | @@ -139,6 +139,11 @@ type Config struct { | ||
| 139 | 139 | // editor can reload one over ACP (session/load). |
| 140 | 140 | Sessions SessionsConfig `yaml:"sessions"` |
| 141 | 141 | |
| 142 | + // Trace: write every request sent to the model and every response, as | |
| 143 | + // they travel on the wire, to files (internal/engine/trace.go). Off | |
| 144 | + // unless `trace.enabled: true` or AGENT_TRACE=true. | |
| 145 | + Trace TraceConfig `yaml:"trace"` | |
| 146 | + | |
| 142 | 147 | // ACP: how the agent introduces itself to an editor in the `initialize` |
| 143 | 148 | // response (`mm -acp`). The version is not configurable: it is the |
| 144 | 149 | // binary's own, the one `mm --version` prints. |
| @@ -375,6 +380,15 @@ func (m MCPConfig) Validate() error { | ||
| 375 | 380 | return nil |
| 376 | 381 | } |
| 377 | 382 | |
| 383 | +// TraceConfig is the `trace:` block of the YAML. | |
| 384 | +type TraceConfig struct { | |
| 385 | + // Enabled turns the trace on. AGENT_TRACE (true/false) overrides it. | |
| 386 | + Enabled bool `yaml:"enabled"` | |
| 387 | + // Dir: where the trace files go, relative to the directory mm runs in; | |
| 388 | + // an absolute path is used as-is. | |
| 389 | + Dir string `yaml:"dir"` | |
| 390 | +} | |
| 391 | + | |
| 378 | 392 | type SessionsConfig struct { |
| 379 | 393 | // Enabled turns the persistence on. Without it nothing is written and |
| 380 | 394 | // ACP does not advertise session/load. |
| @@ -486,6 +500,10 @@ which ones you left running.`, | ||
| 486 | 500 | Enabled: false, |
| 487 | 501 | Dir: ".mm/sessions", |
| 488 | 502 | }, |
| 503 | + Trace: TraceConfig{ | |
| 504 | + Enabled: false, | |
| 505 | + Dir: ".mm/traces", | |
| 506 | + }, | |
| 489 | 507 | ACP: ACPConfig{ |
| 490 | 508 | Name: "bob", |
| 491 | 509 | Title: DefaultACPTitle, |
| @@ -582,9 +600,22 @@ func applyEnv() error { | ||
| 582 | 600 | } |
| 583 | 601 | Cfg.Yolo = yolo |
| 584 | 602 | } |
| 603 | + // AGENT_TRACE: trace one run without editing the file. Not removed from | |
| 604 | + // the environment of an agent started by /ask or ask_agent: tracing a | |
| 605 | + // coordinator traces the agents it asks too, each under its own name. | |
| 606 | + if v := os.Getenv(TraceEnv); v != "" { | |
| 607 | + on, err := strconv.ParseBool(v) | |
| 608 | + if err != nil { | |
| 609 | + return fmt.Errorf("%s=%q: must be true or false", TraceEnv, v) | |
| 610 | + } | |
| 611 | + Cfg.Trace.Enabled = on | |
| 612 | + } | |
| 585 | 613 | return nil |
| 586 | 614 | } |
| 587 | 615 | |
| 616 | +// TraceEnv names the variable that overrides `trace.enabled` for one run. | |
| 617 | +const TraceEnv = "AGENT_TRACE" | |
| 618 | + | |
| 588 | 619 | // applyDeprecated maps the deprecated top-level `permissions` onto Yolo: |
| 589 | 620 | // "ask" was the window's way of asking, which `yolo: false` now means |
| 590 | 621 | // everywhere. It wins over `yolo: true` written beside it — of two |
| @@ -684,6 +715,8 @@ func (c Config) validate() error { | ||
| 684 | 715 | return errors.New("context.maxMessages must be >= 0") |
| 685 | 716 | case c.Sessions.Dir == "": |
| 686 | 717 | return errors.New("sessions.dir must not be empty") |
| 718 | + case c.Trace.Enabled && strings.TrimSpace(c.Trace.Dir) == "": | |
| 719 | + return errors.New("trace.dir must not be empty") | |
| 687 | 720 | case strings.TrimSpace(c.ACP.Name) == "": |
| 688 | 721 | return errors.New("acp.name must not be empty") |
| 689 | 722 | case c.Permissions != "" && c.Permissions != PermissionsAuto && c.Permissions != PermissionsAsk: |
| @@ -139,6 +139,11 @@ type Config struct { | |||
| 139 | // editor can reload one over ACP (session/load). | 139 | // editor can reload one over ACP (session/load). |
| 140 | Sessions SessionsConfig `yaml:"sessions"` | 140 | Sessions SessionsConfig `yaml:"sessions"` |
| 141 | 141 | ||
| 142 | + // Trace: write every request sent to the model and every response, as | ||
| 143 | + // they travel on the wire, to files (internal/engine/trace.go). Off | ||
| 144 | + // unless `trace.enabled: true` or AGENT_TRACE=true. | ||
| 145 | + Trace TraceConfig `yaml:"trace"` | ||
| 146 | + | ||
| 142 | // ACP: how the agent introduces itself to an editor in the `initialize` | 147 | // ACP: how the agent introduces itself to an editor in the `initialize` |
| 143 | // response (`mm -acp`). The version is not configurable: it is the | 148 | // response (`mm -acp`). The version is not configurable: it is the |
| 144 | // binary's own, the one `mm --version` prints. | 149 | // binary's own, the one `mm --version` prints. |
| @@ -375,6 +380,15 @@ func (m MCPConfig) Validate() error { | |||
| 375 | return nil | 380 | return nil |
| 376 | } | 381 | } |
| 377 | 382 | ||
| 383 | +// TraceConfig is the `trace:` block of the YAML. | ||
| 384 | +type TraceConfig struct { | ||
| 385 | + // Enabled turns the trace on. AGENT_TRACE (true/false) overrides it. | ||
| 386 | + Enabled bool `yaml:"enabled"` | ||
| 387 | + // Dir: where the trace files go, relative to the directory mm runs in; | ||
| 388 | + // an absolute path is used as-is. | ||
| 389 | + Dir string `yaml:"dir"` | ||
| 390 | +} | ||
| 391 | + | ||
| 378 | type SessionsConfig struct { | 392 | type SessionsConfig struct { |
| 379 | // Enabled turns the persistence on. Without it nothing is written and | 393 | // Enabled turns the persistence on. Without it nothing is written and |
| 380 | // ACP does not advertise session/load. | 394 | // ACP does not advertise session/load. |
| @@ -486,6 +500,10 @@ which ones you left running.`, | |||
| 486 | Enabled: false, | 500 | Enabled: false, |
| 487 | Dir: ".mm/sessions", | 501 | Dir: ".mm/sessions", |
| 488 | }, | 502 | }, |
| 503 | + Trace: TraceConfig{ | ||
| 504 | + Enabled: false, | ||
| 505 | + Dir: ".mm/traces", | ||
| 506 | + }, | ||
| 489 | ACP: ACPConfig{ | 507 | ACP: ACPConfig{ |
| 490 | Name: "bob", | 508 | Name: "bob", |
| 491 | Title: DefaultACPTitle, | 509 | Title: DefaultACPTitle, |
| @@ -582,9 +600,22 @@ func applyEnv() error { | |||
| 582 | } | 600 | } |
| 583 | Cfg.Yolo = yolo | 601 | Cfg.Yolo = yolo |
| 584 | } | 602 | } |
| 603 | + // AGENT_TRACE: trace one run without editing the file. Not removed from | ||
| 604 | + // the environment of an agent started by /ask or ask_agent: tracing a | ||
| 605 | + // coordinator traces the agents it asks too, each under its own name. | ||
| 606 | + if v := os.Getenv(TraceEnv); v != "" { | ||
| 607 | + on, err := strconv.ParseBool(v) | ||
| 608 | + if err != nil { | ||
| 609 | + return fmt.Errorf("%s=%q: must be true or false", TraceEnv, v) | ||
| 610 | + } | ||
| 611 | + Cfg.Trace.Enabled = on | ||
| 612 | + } | ||
| 585 | return nil | 613 | return nil |
| 586 | } | 614 | } |
| 587 | 615 | ||
| 616 | +// TraceEnv names the variable that overrides `trace.enabled` for one run. | ||
| 617 | +const TraceEnv = "AGENT_TRACE" | ||
| 618 | + | ||
| 588 | // applyDeprecated maps the deprecated top-level `permissions` onto Yolo: | 619 | // applyDeprecated maps the deprecated top-level `permissions` onto Yolo: |
| 589 | // "ask" was the window's way of asking, which `yolo: false` now means | 620 | // "ask" was the window's way of asking, which `yolo: false` now means |
| 590 | // everywhere. It wins over `yolo: true` written beside it — of two | 621 | // everywhere. It wins over `yolo: true` written beside it — of two |
| @@ -684,6 +715,8 @@ func (c Config) validate() error { | |||
| 684 | return errors.New("context.maxMessages must be >= 0") | 715 | return errors.New("context.maxMessages must be >= 0") |
| 685 | case c.Sessions.Dir == "": | 716 | case c.Sessions.Dir == "": |
| 686 | return errors.New("sessions.dir must not be empty") | 717 | return errors.New("sessions.dir must not be empty") |
| 718 | + case c.Trace.Enabled && strings.TrimSpace(c.Trace.Dir) == "": | ||
| 719 | + return errors.New("trace.dir must not be empty") | ||
| 687 | case strings.TrimSpace(c.ACP.Name) == "": | 720 | case strings.TrimSpace(c.ACP.Name) == "": |
| 688 | return errors.New("acp.name must not be empty") | 721 | return errors.New("acp.name must not be empty") |
| 689 | case c.Permissions != "" && c.Permissions != PermissionsAuto && c.Permissions != PermissionsAsk: | 722 | case c.Permissions != "" && c.Permissions != PermissionsAuto && c.Permissions != PermissionsAsk: |
modified
internal/engine/openai_compat.go +12 -1 | @@ -185,7 +185,7 @@ func (p *openaiCompat) Open(ctx context.Context, b Backend) (*genkit.Genkit, str | ||
| 185 | 185 | // on the HTTP client, so a thinking model's reasoning can be read off |
| 186 | 186 | // the wire the plugin drops it from (reasoning.go), and a streamed |
| 187 | 187 | // request asks for the token counts it would otherwise lack (usage.go). |
| 188 | - Opts: []option.RequestOption{option.WithMiddleware(streamOptions(p.progress, p.thinkingOff), reasoningTap)}, | |
| 188 | + Opts: []option.RequestOption{option.WithMiddleware(middlewares(p)...)}, | |
| 189 | 189 | } |
| 190 | 190 | quietGenkit() |
| 191 | 191 | g := genkit.Init(ctx, genkit.WithPlugins(client)) |
| @@ -204,6 +204,17 @@ func (p *openaiCompat) Open(ctx context.Context, b Backend) (*genkit.Genkit, str | ||
| 204 | 204 | return g, p.name + "/" + b.Model, nil |
| 205 | 205 | } |
| 206 | 206 | |
| 207 | +// middlewares is the HTTP client's chain, outermost first. The trace comes | |
| 208 | +// last, next to the network: it records the request as it leaves, after the | |
| 209 | +// others have added their fields, and the response before they read it. | |
| 210 | +func middlewares(p *openaiCompat) []option.Middleware { | |
| 211 | + chain := []option.Middleware{streamOptions(p.progress, p.thinkingOff), reasoningTap} | |
| 212 | + if config.Cfg.Trace.Enabled { | |
| 213 | + chain = append(chain, traceMiddleware(config.Cfg.Trace.Dir, config.Cfg.ACP.Name)) | |
| 214 | + } | |
| 215 | + return chain | |
| 216 | +} | |
| 217 | + | |
| 207 | 218 | // quietGenkit keeps Genkit's informational log lines off the screen: since |
| 208 | 219 | // 1.13 Init logs "INFO Genkit initialized env=prod plugins=[dmr] …" on |
| 209 | 220 | // stderr, which lands in the middle of the terminal's output and of the |
| @@ -185,7 +185,7 @@ func (p *openaiCompat) Open(ctx context.Context, b Backend) (*genkit.Genkit, str | |||
| 185 | // on the HTTP client, so a thinking model's reasoning can be read off | 185 | // on the HTTP client, so a thinking model's reasoning can be read off |
| 186 | // the wire the plugin drops it from (reasoning.go), and a streamed | 186 | // the wire the plugin drops it from (reasoning.go), and a streamed |
| 187 | // request asks for the token counts it would otherwise lack (usage.go). | 187 | // request asks for the token counts it would otherwise lack (usage.go). |
| 188 | - Opts: []option.RequestOption{option.WithMiddleware(streamOptions(p.progress, p.thinkingOff), reasoningTap)}, | 188 | + Opts: []option.RequestOption{option.WithMiddleware(middlewares(p)...)}, |
| 189 | } | 189 | } |
| 190 | quietGenkit() | 190 | quietGenkit() |
| 191 | g := genkit.Init(ctx, genkit.WithPlugins(client)) | 191 | g := genkit.Init(ctx, genkit.WithPlugins(client)) |
| @@ -204,6 +204,17 @@ func (p *openaiCompat) Open(ctx context.Context, b Backend) (*genkit.Genkit, str | |||
| 204 | return g, p.name + "/" + b.Model, nil | 204 | return g, p.name + "/" + b.Model, nil |
| 205 | } | 205 | } |
| 206 | 206 | ||
| 207 | +// middlewares is the HTTP client's chain, outermost first. The trace comes | ||
| 208 | +// last, next to the network: it records the request as it leaves, after the | ||
| 209 | +// others have added their fields, and the response before they read it. | ||
| 210 | +func middlewares(p *openaiCompat) []option.Middleware { | ||
| 211 | + chain := []option.Middleware{streamOptions(p.progress, p.thinkingOff), reasoningTap} | ||
| 212 | + if config.Cfg.Trace.Enabled { | ||
| 213 | + chain = append(chain, traceMiddleware(config.Cfg.Trace.Dir, config.Cfg.ACP.Name)) | ||
| 214 | + } | ||
| 215 | + return chain | ||
| 216 | +} | ||
| 217 | + | ||
| 207 | // quietGenkit keeps Genkit's informational log lines off the screen: since | 218 | // quietGenkit keeps Genkit's informational log lines off the screen: since |
| 208 | // 1.13 Init logs "INFO Genkit initialized env=prod plugins=[dmr] …" on | 219 | // 1.13 Init logs "INFO Genkit initialized env=prod plugins=[dmr] …" on |
| 209 | // stderr, which lands in the middle of the terminal's output and of the | 220 | // stderr, which lands in the middle of the terminal's output and of the |
added
internal/engine/trace.go +167 -0 | new file mode 100644 | ||
| @@ -0,0 +1,167 @@ | ||
| 1 | +package engine | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "encoding/json" | |
| 6 | + "fmt" | |
| 7 | + "io" | |
| 8 | + "net/http" | |
| 9 | + "os" | |
| 10 | + "path/filepath" | |
| 11 | + "strings" | |
| 12 | + "sync" | |
| 13 | + "sync/atomic" | |
| 14 | + "time" | |
| 15 | + | |
| 16 | + "mm/internal/ui" | |
| 17 | + | |
| 18 | + "github.com/openai/openai-go/option" | |
| 19 | +) | |
| 20 | + | |
| 21 | +// The trace writes what goes to the model and what comes back, exactly as it | |
| 22 | +// travels: the request body once mm's own middlewares have added their fields | |
| 23 | +// (stream_options, return_progress, the reasoning switch of a summary), and | |
| 24 | +// the response body byte for byte — a streamed answer as the SSE lines the | |
| 25 | +// server sent, every chunk's JSON included. It is the last middleware, the | |
| 26 | +// one next to the network, so nothing is added after it has looked. | |
| 27 | +// | |
| 28 | +// One pair of files per model call, in trace.dir: | |
| 29 | +// | |
| 30 | +// 20261007-081502.123-bob-0001.request.json the request, indented | |
| 31 | +// 20261007-081502.123-bob-0001.response-200.sse a streamed response | |
| 32 | +// 20261007-081502.123-bob-0002.response-200.json a whole one, indented | |
| 33 | +// 20261007-081502.123-bob-0003.response-error.txt no response: why | |
| 34 | +// | |
| 35 | +// The agent's acp.name is in the name: an agent started by /ask or | |
| 36 | +// ask_agent inherits AGENT_TRACE and writes beside its asker. Headers are | |
| 37 | +// never written: the API key travels in them. | |
| 38 | + | |
| 39 | +// traceSeq numbers the model calls of the process. | |
| 40 | +var traceSeq atomic.Int64 | |
| 41 | + | |
| 42 | +// traceWarned: a trace that cannot be written says so once, not per call. | |
| 43 | +var traceWarned sync.Once | |
| 44 | + | |
| 45 | +// traceMiddleware returns the openai-go middleware writing the trace files | |
| 46 | +// of agent into dir. A file that cannot be written costs the trace, never | |
| 47 | +// the call — but the first failure is reported on ui.Out (stderr in ACP | |
| 48 | +// mode, where an editor shows it in the agent's log). | |
| 49 | +func traceMiddleware(dir, agent string) option.Middleware { | |
| 50 | + return func(req *http.Request, next option.MiddlewareNext) (*http.Response, error) { | |
| 51 | + where := traceDirOf(dir) | |
| 52 | + base := filepath.Join(where, fmt.Sprintf("%s-%s-%04d", time.Now().Format("20060102-150405.000"), agent, traceSeq.Add(1))) | |
| 53 | + if err := os.MkdirAll(where, 0o755); err != nil { | |
| 54 | + traceWarned.Do(func() { fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err) }) | |
| 55 | + } | |
| 56 | + if req.Body != nil { | |
| 57 | + if body, err := io.ReadAll(req.Body); err == nil { | |
| 58 | + _ = req.Body.Close() | |
| 59 | + req.Body = io.NopCloser(bytes.NewReader(body)) | |
| 60 | + req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(body)), nil } | |
| 61 | + _ = os.WriteFile(base+".request.json", indented(body), 0o644) | |
| 62 | + } | |
| 63 | + } | |
| 64 | + resp, err := next(req) | |
| 65 | + if err != nil { | |
| 66 | + _ = os.WriteFile(base+".response-error.txt", []byte(req.Method+" "+req.URL.String()+"\n"+err.Error()+"\n"), 0o644) | |
| 67 | + return resp, err | |
| 68 | + } | |
| 69 | + ext := ".json" | |
| 70 | + if strings.Contains(resp.Header.Get("Content-Type"), "event-stream") { | |
| 71 | + ext = ".sse" | |
| 72 | + } | |
| 73 | + resp.Body = &traceBody{body: resp.Body, path: fmt.Sprintf("%s.response-%d%s", base, resp.StatusCode, ext), stream: ext == ".sse"} | |
| 74 | + return resp, nil | |
| 75 | + } | |
| 76 | +} | |
| 77 | + | |
| 78 | +// traceDirOf is where this call's files go: an absolute dir as it is; a | |
| 79 | +// relative one in the working directory of the turn in progress — the | |
| 80 | +// session's cwd in ACP, like sessions.dir — else in the directory mm runs | |
| 81 | +// in. An editor may launch mm anywhere; the session says which project. | |
| 82 | +func traceDirOf(dir string) string { | |
| 83 | + if !filepath.IsAbs(dir) { | |
| 84 | + dir = ui.Resolve(dir) | |
| 85 | + } | |
| 86 | + if abs, err := filepath.Abs(dir); err == nil { | |
| 87 | + return abs | |
| 88 | + } | |
| 89 | + return dir | |
| 90 | +} | |
| 91 | + | |
| 92 | +// traceBody copies a response body to its trace file as the client reads | |
| 93 | +// it. A stream is written as it arrives — a call that hangs or is cancelled | |
| 94 | +// still leaves what came; a whole JSON body is kept and written indented | |
| 95 | +// when the client is done with it. | |
| 96 | +type traceBody struct { | |
| 97 | + body io.ReadCloser | |
| 98 | + path string | |
| 99 | + stream bool | |
| 100 | + | |
| 101 | + mu sync.Mutex | |
| 102 | + file *os.File // stream: open while reading | |
| 103 | + buf bytes.Buffer // whole body | |
| 104 | + done bool | |
| 105 | +} | |
| 106 | + | |
| 107 | +func (t *traceBody) Read(p []byte) (int, error) { | |
| 108 | + n, err := t.body.Read(p) | |
| 109 | + if n > 0 { | |
| 110 | + t.keep(p[:n]) | |
| 111 | + } | |
| 112 | + if err == io.EOF { | |
| 113 | + t.finish() | |
| 114 | + } | |
| 115 | + return n, err | |
| 116 | +} | |
| 117 | + | |
| 118 | +func (t *traceBody) Close() error { | |
| 119 | + t.finish() | |
| 120 | + return t.body.Close() | |
| 121 | +} | |
| 122 | + | |
| 123 | +// keep records one piece of the body. | |
| 124 | +func (t *traceBody) keep(b []byte) { | |
| 125 | + t.mu.Lock() | |
| 126 | + defer t.mu.Unlock() | |
| 127 | + if !t.stream { | |
| 128 | + t.buf.Write(b) | |
| 129 | + return | |
| 130 | + } | |
| 131 | + if t.file == nil && !t.done { | |
| 132 | + t.file, _ = os.Create(t.path) | |
| 133 | + } | |
| 134 | + if t.file != nil { | |
| 135 | + _, _ = t.file.Write(b) | |
| 136 | + } | |
| 137 | +} | |
| 138 | + | |
| 139 | +// finish closes the stream's file, or writes the whole body. Once. | |
| 140 | +func (t *traceBody) finish() { | |
| 141 | + t.mu.Lock() | |
| 142 | + defer t.mu.Unlock() | |
| 143 | + if t.done { | |
| 144 | + return | |
| 145 | + } | |
| 146 | + t.done = true | |
| 147 | + if t.stream { | |
| 148 | + if t.file == nil { // nothing came: still say so | |
| 149 | + t.file, _ = os.Create(t.path) | |
| 150 | + } | |
| 151 | + if t.file != nil { | |
| 152 | + _ = t.file.Close() | |
| 153 | + } | |
| 154 | + return | |
| 155 | + } | |
| 156 | + _ = os.WriteFile(t.path, indented(t.buf.Bytes()), 0o644) | |
| 157 | +} | |
| 158 | + | |
| 159 | +// indented is body as indented JSON, or body unchanged when it is not JSON. | |
| 160 | +func indented(body []byte) []byte { | |
| 161 | + var out bytes.Buffer | |
| 162 | + if json.Indent(&out, body, "", " ") != nil { | |
| 163 | + return body | |
| 164 | + } | |
| 165 | + out.WriteByte('\n') | |
| 166 | + return out.Bytes() | |
| 167 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,167 @@ | |||
| 1 | +package engine | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "bytes" | ||
| 5 | + "encoding/json" | ||
| 6 | + "fmt" | ||
| 7 | + "io" | ||
| 8 | + "net/http" | ||
| 9 | + "os" | ||
| 10 | + "path/filepath" | ||
| 11 | + "strings" | ||
| 12 | + "sync" | ||
| 13 | + "sync/atomic" | ||
| 14 | + "time" | ||
| 15 | + | ||
| 16 | + "mm/internal/ui" | ||
| 17 | + | ||
| 18 | + "github.com/openai/openai-go/option" | ||
| 19 | +) | ||
| 20 | + | ||
| 21 | +// The trace writes what goes to the model and what comes back, exactly as it | ||
| 22 | +// travels: the request body once mm's own middlewares have added their fields | ||
| 23 | +// (stream_options, return_progress, the reasoning switch of a summary), and | ||
| 24 | +// the response body byte for byte — a streamed answer as the SSE lines the | ||
| 25 | +// server sent, every chunk's JSON included. It is the last middleware, the | ||
| 26 | +// one next to the network, so nothing is added after it has looked. | ||
| 27 | +// | ||
| 28 | +// One pair of files per model call, in trace.dir: | ||
| 29 | +// | ||
| 30 | +// 20261007-081502.123-bob-0001.request.json the request, indented | ||
| 31 | +// 20261007-081502.123-bob-0001.response-200.sse a streamed response | ||
| 32 | +// 20261007-081502.123-bob-0002.response-200.json a whole one, indented | ||
| 33 | +// 20261007-081502.123-bob-0003.response-error.txt no response: why | ||
| 34 | +// | ||
| 35 | +// The agent's acp.name is in the name: an agent started by /ask or | ||
| 36 | +// ask_agent inherits AGENT_TRACE and writes beside its asker. Headers are | ||
| 37 | +// never written: the API key travels in them. | ||
| 38 | + | ||
| 39 | +// traceSeq numbers the model calls of the process. | ||
| 40 | +var traceSeq atomic.Int64 | ||
| 41 | + | ||
| 42 | +// traceWarned: a trace that cannot be written says so once, not per call. | ||
| 43 | +var traceWarned sync.Once | ||
| 44 | + | ||
| 45 | +// traceMiddleware returns the openai-go middleware writing the trace files | ||
| 46 | +// of agent into dir. A file that cannot be written costs the trace, never | ||
| 47 | +// the call — but the first failure is reported on ui.Out (stderr in ACP | ||
| 48 | +// mode, where an editor shows it in the agent's log). | ||
| 49 | +func traceMiddleware(dir, agent string) option.Middleware { | ||
| 50 | + return func(req *http.Request, next option.MiddlewareNext) (*http.Response, error) { | ||
| 51 | + where := traceDirOf(dir) | ||
| 52 | + base := filepath.Join(where, fmt.Sprintf("%s-%s-%04d", time.Now().Format("20060102-150405.000"), agent, traceSeq.Add(1))) | ||
| 53 | + if err := os.MkdirAll(where, 0o755); err != nil { | ||
| 54 | + traceWarned.Do(func() { fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err) }) | ||
| 55 | + } | ||
| 56 | + if req.Body != nil { | ||
| 57 | + if body, err := io.ReadAll(req.Body); err == nil { | ||
| 58 | + _ = req.Body.Close() | ||
| 59 | + req.Body = io.NopCloser(bytes.NewReader(body)) | ||
| 60 | + req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(body)), nil } | ||
| 61 | + _ = os.WriteFile(base+".request.json", indented(body), 0o644) | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + resp, err := next(req) | ||
| 65 | + if err != nil { | ||
| 66 | + _ = os.WriteFile(base+".response-error.txt", []byte(req.Method+" "+req.URL.String()+"\n"+err.Error()+"\n"), 0o644) | ||
| 67 | + return resp, err | ||
| 68 | + } | ||
| 69 | + ext := ".json" | ||
| 70 | + if strings.Contains(resp.Header.Get("Content-Type"), "event-stream") { | ||
| 71 | + ext = ".sse" | ||
| 72 | + } | ||
| 73 | + resp.Body = &traceBody{body: resp.Body, path: fmt.Sprintf("%s.response-%d%s", base, resp.StatusCode, ext), stream: ext == ".sse"} | ||
| 74 | + return resp, nil | ||
| 75 | + } | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +// traceDirOf is where this call's files go: an absolute dir as it is; a | ||
| 79 | +// relative one in the working directory of the turn in progress — the | ||
| 80 | +// session's cwd in ACP, like sessions.dir — else in the directory mm runs | ||
| 81 | +// in. An editor may launch mm anywhere; the session says which project. | ||
| 82 | +func traceDirOf(dir string) string { | ||
| 83 | + if !filepath.IsAbs(dir) { | ||
| 84 | + dir = ui.Resolve(dir) | ||
| 85 | + } | ||
| 86 | + if abs, err := filepath.Abs(dir); err == nil { | ||
| 87 | + return abs | ||
| 88 | + } | ||
| 89 | + return dir | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +// traceBody copies a response body to its trace file as the client reads | ||
| 93 | +// it. A stream is written as it arrives — a call that hangs or is cancelled | ||
| 94 | +// still leaves what came; a whole JSON body is kept and written indented | ||
| 95 | +// when the client is done with it. | ||
| 96 | +type traceBody struct { | ||
| 97 | + body io.ReadCloser | ||
| 98 | + path string | ||
| 99 | + stream bool | ||
| 100 | + | ||
| 101 | + mu sync.Mutex | ||
| 102 | + file *os.File // stream: open while reading | ||
| 103 | + buf bytes.Buffer // whole body | ||
| 104 | + done bool | ||
| 105 | +} | ||
| 106 | + | ||
| 107 | +func (t *traceBody) Read(p []byte) (int, error) { | ||
| 108 | + n, err := t.body.Read(p) | ||
| 109 | + if n > 0 { | ||
| 110 | + t.keep(p[:n]) | ||
| 111 | + } | ||
| 112 | + if err == io.EOF { | ||
| 113 | + t.finish() | ||
| 114 | + } | ||
| 115 | + return n, err | ||
| 116 | +} | ||
| 117 | + | ||
| 118 | +func (t *traceBody) Close() error { | ||
| 119 | + t.finish() | ||
| 120 | + return t.body.Close() | ||
| 121 | +} | ||
| 122 | + | ||
| 123 | +// keep records one piece of the body. | ||
| 124 | +func (t *traceBody) keep(b []byte) { | ||
| 125 | + t.mu.Lock() | ||
| 126 | + defer t.mu.Unlock() | ||
| 127 | + if !t.stream { | ||
| 128 | + t.buf.Write(b) | ||
| 129 | + return | ||
| 130 | + } | ||
| 131 | + if t.file == nil && !t.done { | ||
| 132 | + t.file, _ = os.Create(t.path) | ||
| 133 | + } | ||
| 134 | + if t.file != nil { | ||
| 135 | + _, _ = t.file.Write(b) | ||
| 136 | + } | ||
| 137 | +} | ||
| 138 | + | ||
| 139 | +// finish closes the stream's file, or writes the whole body. Once. | ||
| 140 | +func (t *traceBody) finish() { | ||
| 141 | + t.mu.Lock() | ||
| 142 | + defer t.mu.Unlock() | ||
| 143 | + if t.done { | ||
| 144 | + return | ||
| 145 | + } | ||
| 146 | + t.done = true | ||
| 147 | + if t.stream { | ||
| 148 | + if t.file == nil { // nothing came: still say so | ||
| 149 | + t.file, _ = os.Create(t.path) | ||
| 150 | + } | ||
| 151 | + if t.file != nil { | ||
| 152 | + _ = t.file.Close() | ||
| 153 | + } | ||
| 154 | + return | ||
| 155 | + } | ||
| 156 | + _ = os.WriteFile(t.path, indented(t.buf.Bytes()), 0o644) | ||
| 157 | +} | ||
| 158 | + | ||
| 159 | +// indented is body as indented JSON, or body unchanged when it is not JSON. | ||
| 160 | +func indented(body []byte) []byte { | ||
| 161 | + var out bytes.Buffer | ||
| 162 | + if json.Indent(&out, body, "", " ") != nil { | ||
| 163 | + return body | ||
| 164 | + } | ||
| 165 | + out.WriteByte('\n') | ||
| 166 | + return out.Bytes() | ||
| 167 | +} | ||
added
internal/engine/trace_test.go +182 -0 | new file mode 100644 | ||
| @@ -0,0 +1,182 @@ | ||
| 1 | +package engine | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "errors" | |
| 6 | + "io" | |
| 7 | + "net/http" | |
| 8 | + "net/http/httptest" | |
| 9 | + "os" | |
| 10 | + "path/filepath" | |
| 11 | + "strings" | |
| 12 | + "sync" | |
| 13 | + "testing" | |
| 14 | + | |
| 15 | + "mm/internal/ui" | |
| 16 | +) | |
| 17 | + | |
| 18 | +// traceFiles returns the trace dir's files, name -> content. | |
| 19 | +func traceFiles(t *testing.T, dir string) map[string]string { | |
| 20 | + t.Helper() | |
| 21 | + entries, err := os.ReadDir(dir) | |
| 22 | + if err != nil { | |
| 23 | + t.Fatal(err) | |
| 24 | + } | |
| 25 | + files := map[string]string{} | |
| 26 | + for _, e := range entries { | |
| 27 | + b, _ := os.ReadFile(filepath.Join(dir, e.Name())) | |
| 28 | + files[e.Name()] = string(b) | |
| 29 | + } | |
| 30 | + return files | |
| 31 | +} | |
| 32 | + | |
| 33 | +// traced sends body through the trace middleware to srv and reads the whole | |
| 34 | +// response, as the SDK would. | |
| 35 | +func traced(t *testing.T, dir, url, body string) { | |
| 36 | + t.Helper() | |
| 37 | + req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(body)) | |
| 38 | + req.Header.Set("Authorization", "Bearer sk-secret") | |
| 39 | + resp, err := traceMiddleware(dir, "bob")(req, http.DefaultClient.Do) | |
| 40 | + if err != nil { | |
| 41 | + t.Fatal(err) | |
| 42 | + } | |
| 43 | + _, _ = io.ReadAll(resp.Body) | |
| 44 | + _ = resp.Body.Close() | |
| 45 | +} | |
| 46 | + | |
| 47 | +// A streamed call: the request as sent (indented), the SSE as received, | |
| 48 | +// named after the agent and the status; the key is nowhere. | |
| 49 | +func TestTraceStreamedCall(t *testing.T) { | |
| 50 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 51 | + got, _ := io.ReadAll(r.Body) | |
| 52 | + if string(got) != `{"model":"m","stream":true}` { | |
| 53 | + t.Errorf("the server got %q: the trace changed the request", got) | |
| 54 | + } | |
| 55 | + w.Header().Set("Content-Type", "text/event-stream") | |
| 56 | + _, _ = io.WriteString(w, "data: {\"choices\":[{\"delta\":{\"content\":\"hi\"}}]}\n\ndata: [DONE]\n\n") | |
| 57 | + })) | |
| 58 | + t.Cleanup(srv.Close) | |
| 59 | + dir := t.TempDir() | |
| 60 | + | |
| 61 | + traced(t, dir, srv.URL, `{"model":"m","stream":true}`) | |
| 62 | + | |
| 63 | + files := traceFiles(t, dir) | |
| 64 | + var request, response string | |
| 65 | + for name, content := range files { | |
| 66 | + switch { | |
| 67 | + case strings.Contains(name, "-bob-") && strings.HasSuffix(name, ".request.json"): | |
| 68 | + request = content | |
| 69 | + case strings.Contains(name, "-bob-") && strings.HasSuffix(name, ".response-200.sse"): | |
| 70 | + response = content | |
| 71 | + } | |
| 72 | + } | |
| 73 | + if request != "{\n \"model\": \"m\",\n \"stream\": true\n}\n" { | |
| 74 | + t.Errorf("request file %q (files %v)", request, keys(files)) | |
| 75 | + } | |
| 76 | + if !strings.Contains(response, `data: {"choices":[{"delta":{"content":"hi"}}]}`) || !strings.Contains(response, "[DONE]") { | |
| 77 | + t.Errorf("response file %q (files %v)", response, keys(files)) | |
| 78 | + } | |
| 79 | + for name, content := range files { | |
| 80 | + if strings.Contains(content, "sk-secret") { | |
| 81 | + t.Errorf("%s carries the API key", name) | |
| 82 | + } | |
| 83 | + } | |
| 84 | +} | |
| 85 | + | |
| 86 | +// A whole JSON answer is indented; an error status is in the file's name. | |
| 87 | +func TestTraceJSONErrorResponse(t *testing.T) { | |
| 88 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 89 | + w.Header().Set("Content-Type", "application/json") | |
| 90 | + w.WriteHeader(http.StatusBadRequest) | |
| 91 | + _, _ = io.WriteString(w, `{"error":{"message":"exceeds the available context size"}}`) | |
| 92 | + })) | |
| 93 | + t.Cleanup(srv.Close) | |
| 94 | + dir := t.TempDir() | |
| 95 | + | |
| 96 | + traced(t, dir, srv.URL, `{"model":"m"}`) | |
| 97 | + | |
| 98 | + for name, content := range traceFiles(t, dir) { | |
| 99 | + if strings.HasSuffix(name, ".response-400.json") { | |
| 100 | + if !strings.Contains(content, "\n \"error\": {") { | |
| 101 | + t.Errorf("not indented: %q", content) | |
| 102 | + } | |
| 103 | + return | |
| 104 | + } | |
| 105 | + } | |
| 106 | + t.Errorf("no .response-400.json among %v", keys(traceFiles(t, dir))) | |
| 107 | +} | |
| 108 | + | |
| 109 | +// No response at all: the reason is written, and the error goes on as is. | |
| 110 | +func TestTraceTransportError(t *testing.T) { | |
| 111 | + dir := t.TempDir() | |
| 112 | + req, _ := http.NewRequest(http.MethodPost, "http://127.0.0.1:1/v1/chat/completions", strings.NewReader(`{}`)) | |
| 113 | + boom := errors.New("connection refused") | |
| 114 | + _, err := traceMiddleware(dir, "bob")(req, func(*http.Request) (*http.Response, error) { return nil, boom }) | |
| 115 | + if !errors.Is(err, boom) { | |
| 116 | + t.Fatalf("err = %v", err) | |
| 117 | + } | |
| 118 | + for name, content := range traceFiles(t, dir) { | |
| 119 | + if strings.HasSuffix(name, ".response-error.txt") && strings.Contains(content, "connection refused") { | |
| 120 | + return | |
| 121 | + } | |
| 122 | + } | |
| 123 | + t.Errorf("no .response-error.txt among %v", keys(traceFiles(t, dir))) | |
| 124 | +} | |
| 125 | + | |
| 126 | +func keys(m map[string]string) []string { | |
| 127 | + var out []string | |
| 128 | + for k := range m { | |
| 129 | + out = append(out, k) | |
| 130 | + } | |
| 131 | + return out | |
| 132 | +} | |
| 133 | + | |
| 134 | +// sessionSink is the active sink of an ACP session: only its WorkDir matters. | |
| 135 | +type sessionSink struct { | |
| 136 | + ui.Sink | |
| 137 | + dir string | |
| 138 | +} | |
| 139 | + | |
| 140 | +func (s sessionSink) WorkDir() string { return s.dir } | |
| 141 | + | |
| 142 | +// A relative trace.dir lands in the session's cwd — where an editor's | |
| 143 | +// project is — not in the directory the editor launched mm from. | |
| 144 | +func TestTraceFollowsTheSessionDirectory(t *testing.T) { | |
| 145 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 146 | + _, _ = io.WriteString(w, `{}`) | |
| 147 | + })) | |
| 148 | + t.Cleanup(srv.Close) | |
| 149 | + project := t.TempDir() | |
| 150 | + ui.SetActive(sessionSink{dir: project}) | |
| 151 | + t.Cleanup(func() { ui.SetActive(nil) }) | |
| 152 | + | |
| 153 | + traced(t, ".mm/traces", srv.URL, `{"model":"m"}`) | |
| 154 | + | |
| 155 | + if files := traceFiles(t, filepath.Join(project, ".mm", "traces")); len(files) != 2 { | |
| 156 | + t.Errorf("files in the session's .mm/traces: %v", keys(files)) | |
| 157 | + } | |
| 158 | +} | |
| 159 | + | |
| 160 | +// A directory that cannot be created is said once, and the call goes on. | |
| 161 | +func TestTraceSaysWhenItCannotWrite(t *testing.T) { | |
| 162 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 163 | + _, _ = io.WriteString(w, `{}`) | |
| 164 | + })) | |
| 165 | + t.Cleanup(srv.Close) | |
| 166 | + blocker := filepath.Join(t.TempDir(), "a-file") | |
| 167 | + if err := os.WriteFile(blocker, nil, 0o644); err != nil { | |
| 168 | + t.Fatal(err) | |
| 169 | + } | |
| 170 | + var out bytes.Buffer | |
| 171 | + oldOut := ui.Out | |
| 172 | + ui.Out = &out | |
| 173 | + traceWarned = sync.Once{} | |
| 174 | + t.Cleanup(func() { ui.Out = oldOut; traceWarned = sync.Once{} }) | |
| 175 | + | |
| 176 | + traced(t, filepath.Join(blocker, "traces"), srv.URL, `{}`) | |
| 177 | + traced(t, filepath.Join(blocker, "traces"), srv.URL, `{}`) | |
| 178 | + | |
| 179 | + if got := out.String(); strings.Count(got, "[trace: cannot write to") != 1 { | |
| 180 | + t.Errorf("warnings: %q", got) | |
| 181 | + } | |
| 182 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,182 @@ | |||
| 1 | +package engine | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "bytes" | ||
| 5 | + "errors" | ||
| 6 | + "io" | ||
| 7 | + "net/http" | ||
| 8 | + "net/http/httptest" | ||
| 9 | + "os" | ||
| 10 | + "path/filepath" | ||
| 11 | + "strings" | ||
| 12 | + "sync" | ||
| 13 | + "testing" | ||
| 14 | + | ||
| 15 | + "mm/internal/ui" | ||
| 16 | +) | ||
| 17 | + | ||
| 18 | +// traceFiles returns the trace dir's files, name -> content. | ||
| 19 | +func traceFiles(t *testing.T, dir string) map[string]string { | ||
| 20 | + t.Helper() | ||
| 21 | + entries, err := os.ReadDir(dir) | ||
| 22 | + if err != nil { | ||
| 23 | + t.Fatal(err) | ||
| 24 | + } | ||
| 25 | + files := map[string]string{} | ||
| 26 | + for _, e := range entries { | ||
| 27 | + b, _ := os.ReadFile(filepath.Join(dir, e.Name())) | ||
| 28 | + files[e.Name()] = string(b) | ||
| 29 | + } | ||
| 30 | + return files | ||
| 31 | +} | ||
| 32 | + | ||
| 33 | +// traced sends body through the trace middleware to srv and reads the whole | ||
| 34 | +// response, as the SDK would. | ||
| 35 | +func traced(t *testing.T, dir, url, body string) { | ||
| 36 | + t.Helper() | ||
| 37 | + req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(body)) | ||
| 38 | + req.Header.Set("Authorization", "Bearer sk-secret") | ||
| 39 | + resp, err := traceMiddleware(dir, "bob")(req, http.DefaultClient.Do) | ||
| 40 | + if err != nil { | ||
| 41 | + t.Fatal(err) | ||
| 42 | + } | ||
| 43 | + _, _ = io.ReadAll(resp.Body) | ||
| 44 | + _ = resp.Body.Close() | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +// A streamed call: the request as sent (indented), the SSE as received, | ||
| 48 | +// named after the agent and the status; the key is nowhere. | ||
| 49 | +func TestTraceStreamedCall(t *testing.T) { | ||
| 50 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 51 | + got, _ := io.ReadAll(r.Body) | ||
| 52 | + if string(got) != `{"model":"m","stream":true}` { | ||
| 53 | + t.Errorf("the server got %q: the trace changed the request", got) | ||
| 54 | + } | ||
| 55 | + w.Header().Set("Content-Type", "text/event-stream") | ||
| 56 | + _, _ = io.WriteString(w, "data: {\"choices\":[{\"delta\":{\"content\":\"hi\"}}]}\n\ndata: [DONE]\n\n") | ||
| 57 | + })) | ||
| 58 | + t.Cleanup(srv.Close) | ||
| 59 | + dir := t.TempDir() | ||
| 60 | + | ||
| 61 | + traced(t, dir, srv.URL, `{"model":"m","stream":true}`) | ||
| 62 | + | ||
| 63 | + files := traceFiles(t, dir) | ||
| 64 | + var request, response string | ||
| 65 | + for name, content := range files { | ||
| 66 | + switch { | ||
| 67 | + case strings.Contains(name, "-bob-") && strings.HasSuffix(name, ".request.json"): | ||
| 68 | + request = content | ||
| 69 | + case strings.Contains(name, "-bob-") && strings.HasSuffix(name, ".response-200.sse"): | ||
| 70 | + response = content | ||
| 71 | + } | ||
| 72 | + } | ||
| 73 | + if request != "{\n \"model\": \"m\",\n \"stream\": true\n}\n" { | ||
| 74 | + t.Errorf("request file %q (files %v)", request, keys(files)) | ||
| 75 | + } | ||
| 76 | + if !strings.Contains(response, `data: {"choices":[{"delta":{"content":"hi"}}]}`) || !strings.Contains(response, "[DONE]") { | ||
| 77 | + t.Errorf("response file %q (files %v)", response, keys(files)) | ||
| 78 | + } | ||
| 79 | + for name, content := range files { | ||
| 80 | + if strings.Contains(content, "sk-secret") { | ||
| 81 | + t.Errorf("%s carries the API key", name) | ||
| 82 | + } | ||
| 83 | + } | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +// A whole JSON answer is indented; an error status is in the file's name. | ||
| 87 | +func TestTraceJSONErrorResponse(t *testing.T) { | ||
| 88 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | ||
| 89 | + w.Header().Set("Content-Type", "application/json") | ||
| 90 | + w.WriteHeader(http.StatusBadRequest) | ||
| 91 | + _, _ = io.WriteString(w, `{"error":{"message":"exceeds the available context size"}}`) | ||
| 92 | + })) | ||
| 93 | + t.Cleanup(srv.Close) | ||
| 94 | + dir := t.TempDir() | ||
| 95 | + | ||
| 96 | + traced(t, dir, srv.URL, `{"model":"m"}`) | ||
| 97 | + | ||
| 98 | + for name, content := range traceFiles(t, dir) { | ||
| 99 | + if strings.HasSuffix(name, ".response-400.json") { | ||
| 100 | + if !strings.Contains(content, "\n \"error\": {") { | ||
| 101 | + t.Errorf("not indented: %q", content) | ||
| 102 | + } | ||
| 103 | + return | ||
| 104 | + } | ||
| 105 | + } | ||
| 106 | + t.Errorf("no .response-400.json among %v", keys(traceFiles(t, dir))) | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +// No response at all: the reason is written, and the error goes on as is. | ||
| 110 | +func TestTraceTransportError(t *testing.T) { | ||
| 111 | + dir := t.TempDir() | ||
| 112 | + req, _ := http.NewRequest(http.MethodPost, "http://127.0.0.1:1/v1/chat/completions", strings.NewReader(`{}`)) | ||
| 113 | + boom := errors.New("connection refused") | ||
| 114 | + _, err := traceMiddleware(dir, "bob")(req, func(*http.Request) (*http.Response, error) { return nil, boom }) | ||
| 115 | + if !errors.Is(err, boom) { | ||
| 116 | + t.Fatalf("err = %v", err) | ||
| 117 | + } | ||
| 118 | + for name, content := range traceFiles(t, dir) { | ||
| 119 | + if strings.HasSuffix(name, ".response-error.txt") && strings.Contains(content, "connection refused") { | ||
| 120 | + return | ||
| 121 | + } | ||
| 122 | + } | ||
| 123 | + t.Errorf("no .response-error.txt among %v", keys(traceFiles(t, dir))) | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +func keys(m map[string]string) []string { | ||
| 127 | + var out []string | ||
| 128 | + for k := range m { | ||
| 129 | + out = append(out, k) | ||
| 130 | + } | ||
| 131 | + return out | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +// sessionSink is the active sink of an ACP session: only its WorkDir matters. | ||
| 135 | +type sessionSink struct { | ||
| 136 | + ui.Sink | ||
| 137 | + dir string | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +func (s sessionSink) WorkDir() string { return s.dir } | ||
| 141 | + | ||
| 142 | +// A relative trace.dir lands in the session's cwd — where an editor's | ||
| 143 | +// project is — not in the directory the editor launched mm from. | ||
| 144 | +func TestTraceFollowsTheSessionDirectory(t *testing.T) { | ||
| 145 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | ||
| 146 | + _, _ = io.WriteString(w, `{}`) | ||
| 147 | + })) | ||
| 148 | + t.Cleanup(srv.Close) | ||
| 149 | + project := t.TempDir() | ||
| 150 | + ui.SetActive(sessionSink{dir: project}) | ||
| 151 | + t.Cleanup(func() { ui.SetActive(nil) }) | ||
| 152 | + | ||
| 153 | + traced(t, ".mm/traces", srv.URL, `{"model":"m"}`) | ||
| 154 | + | ||
| 155 | + if files := traceFiles(t, filepath.Join(project, ".mm", "traces")); len(files) != 2 { | ||
| 156 | + t.Errorf("files in the session's .mm/traces: %v", keys(files)) | ||
| 157 | + } | ||
| 158 | +} | ||
| 159 | + | ||
| 160 | +// A directory that cannot be created is said once, and the call goes on. | ||
| 161 | +func TestTraceSaysWhenItCannotWrite(t *testing.T) { | ||
| 162 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | ||
| 163 | + _, _ = io.WriteString(w, `{}`) | ||
| 164 | + })) | ||
| 165 | + t.Cleanup(srv.Close) | ||
| 166 | + blocker := filepath.Join(t.TempDir(), "a-file") | ||
| 167 | + if err := os.WriteFile(blocker, nil, 0o644); err != nil { | ||
| 168 | + t.Fatal(err) | ||
| 169 | + } | ||
| 170 | + var out bytes.Buffer | ||
| 171 | + oldOut := ui.Out | ||
| 172 | + ui.Out = &out | ||
| 173 | + traceWarned = sync.Once{} | ||
| 174 | + t.Cleanup(func() { ui.Out = oldOut; traceWarned = sync.Once{} }) | ||
| 175 | + | ||
| 176 | + traced(t, filepath.Join(blocker, "traces"), srv.URL, `{}`) | ||
| 177 | + traced(t, filepath.Join(blocker, "traces"), srv.URL, `{}`) | ||
| 178 | + | ||
| 179 | + if got := out.String(); strings.Count(got, "[trace: cannot write to") != 1 { | ||
| 180 | + t.Errorf("warnings: %q", got) | ||
| 181 | + } | ||
| 182 | +} | ||
modified
main.go +24 -4 | @@ -242,7 +242,7 @@ func main() { | ||
| 242 | 242 | return |
| 243 | 243 | } |
| 244 | 244 | |
| 245 | - printBanner(ctx, e, path, set) | |
| 245 | + printBanner(ctx, e, path, set, *acpMode) | |
| 246 | 246 | |
| 247 | 247 | // Three front ends, one wiring: everything above — config, engine, tools, |
| 248 | 248 | // detector — is strictly identical whichever loop runs below. |
| @@ -481,7 +481,7 @@ func assembleMCP(e *engine.Engine, d *detector.LoopDetector, set *toolSet) { | ||
| 481 | 481 | // banner. The probe runs before the banner so its warnings sit right under |
| 482 | 482 | // the prompt they explain; it never stops the agent — on a demo machine the |
| 483 | 483 | // server is often started AFTER the agent. |
| 484 | -func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet) { | |
| 484 | +func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet, acpMode bool) { | |
| 485 | 485 | toolNames, skillCount := set.names, set.skillCount |
| 486 | 486 | info := e.Probe(ctx) |
| 487 | 487 | info.Warnings = append(info.Warnings, set.warnings...) |
| @@ -554,8 +554,8 @@ func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet | ||
| 554 | 554 | // The version leads the line: it is the first thing anyone is asked for |
| 555 | 555 | // when something behaves oddly, and the banner is the one output every |
| 556 | 556 | // front end shows — the window keeps it as the conversation's first line. |
| 557 | - say("%smm %s | config: %s | provider: %s | model: %s | ctx: %s | tools: %s | skills: %d | %s%s", | |
| 558 | - dim, version.Short(), path, e.Backend.Provider, config.Cfg.Model, ctxCol, toolCol, skillCount, yoloColumn(), off) | |
| 557 | + say("%smm %s | config: %s | provider: %s | model: %s | ctx: %s | tools: %s | skills: %d | %s%s%s", | |
| 558 | + dim, version.Short(), path, e.Backend.Provider, config.Cfg.Model, ctxCol, toolCol, skillCount, yoloColumn(), traceColumn(acpMode), off) | |
| 559 | 559 | } |
| 560 | 560 | |
| 561 | 561 | // yoloColumn says in the banner whether the tools run unasked — the one |
| @@ -567,6 +567,26 @@ func yoloColumn() string { | ||
| 567 | 567 | return "yolo: off (tools ask)" |
| 568 | 568 | } |
| 569 | 569 | |
| 570 | +// traceColumn says in the banner that every request and response is being | |
| 571 | +// written to disk — prompts included — and where. In ACP mode a relative | |
| 572 | +// dir is in each session's cwd, which no session has given yet. | |
| 573 | +func traceColumn(acpMode bool) string { | |
| 574 | + if !config.Cfg.Trace.Enabled { | |
| 575 | + return "" | |
| 576 | + } | |
| 577 | + dir := config.Cfg.Trace.Dir | |
| 578 | + if filepath.IsAbs(dir) { | |
| 579 | + return " | trace: " + dir | |
| 580 | + } | |
| 581 | + if acpMode { | |
| 582 | + return " | trace: <session cwd>/" + dir | |
| 583 | + } | |
| 584 | + if abs, err := filepath.Abs(dir); err == nil { | |
| 585 | + dir = abs | |
| 586 | + } | |
| 587 | + return " | trace: " + dir | |
| 588 | +} | |
| 589 | + | |
| 570 | 590 | // commandNames is the slash commands' names, for the shadowing check. |
| 571 | 591 | func commandNames() []string { |
| 572 | 592 | var out []string |
| @@ -242,7 +242,7 @@ func main() { | |||
| 242 | return | 242 | return |
| 243 | } | 243 | } |
| 244 | 244 | ||
| 245 | - printBanner(ctx, e, path, set) | 245 | + printBanner(ctx, e, path, set, *acpMode) |
| 246 | 246 | ||
| 247 | // Three front ends, one wiring: everything above — config, engine, tools, | 247 | // Three front ends, one wiring: everything above — config, engine, tools, |
| 248 | // detector — is strictly identical whichever loop runs below. | 248 | // detector — is strictly identical whichever loop runs below. |
| @@ -481,7 +481,7 @@ func assembleMCP(e *engine.Engine, d *detector.LoopDetector, set *toolSet) { | |||
| 481 | // banner. The probe runs before the banner so its warnings sit right under | 481 | // banner. The probe runs before the banner so its warnings sit right under |
| 482 | // the prompt they explain; it never stops the agent — on a demo machine the | 482 | // the prompt they explain; it never stops the agent — on a demo machine the |
| 483 | // server is often started AFTER the agent. | 483 | // server is often started AFTER the agent. |
| 484 | -func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet) { | 484 | +func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet, acpMode bool) { |
| 485 | toolNames, skillCount := set.names, set.skillCount | 485 | toolNames, skillCount := set.names, set.skillCount |
| 486 | info := e.Probe(ctx) | 486 | info := e.Probe(ctx) |
| 487 | info.Warnings = append(info.Warnings, set.warnings...) | 487 | info.Warnings = append(info.Warnings, set.warnings...) |
| @@ -554,8 +554,8 @@ func printBanner(ctx context.Context, e *engine.Engine, path string, set toolSet | |||
| 554 | // The version leads the line: it is the first thing anyone is asked for | 554 | // The version leads the line: it is the first thing anyone is asked for |
| 555 | // when something behaves oddly, and the banner is the one output every | 555 | // when something behaves oddly, and the banner is the one output every |
| 556 | // front end shows — the window keeps it as the conversation's first line. | 556 | // front end shows — the window keeps it as the conversation's first line. |
| 557 | - say("%smm %s | config: %s | provider: %s | model: %s | ctx: %s | tools: %s | skills: %d | %s%s", | 557 | + say("%smm %s | config: %s | provider: %s | model: %s | ctx: %s | tools: %s | skills: %d | %s%s%s", |
| 558 | - dim, version.Short(), path, e.Backend.Provider, config.Cfg.Model, ctxCol, toolCol, skillCount, yoloColumn(), off) | 558 | + dim, version.Short(), path, e.Backend.Provider, config.Cfg.Model, ctxCol, toolCol, skillCount, yoloColumn(), traceColumn(acpMode), off) |
| 559 | } | 559 | } |
| 560 | 560 | ||
| 561 | // yoloColumn says in the banner whether the tools run unasked — the one | 561 | // yoloColumn says in the banner whether the tools run unasked — the one |
| @@ -567,6 +567,26 @@ func yoloColumn() string { | |||
| 567 | return "yolo: off (tools ask)" | 567 | return "yolo: off (tools ask)" |
| 568 | } | 568 | } |
| 569 | 569 | ||
| 570 | +// traceColumn says in the banner that every request and response is being | ||
| 571 | +// written to disk — prompts included — and where. In ACP mode a relative | ||
| 572 | +// dir is in each session's cwd, which no session has given yet. | ||
| 573 | +func traceColumn(acpMode bool) string { | ||
| 574 | + if !config.Cfg.Trace.Enabled { | ||
| 575 | + return "" | ||
| 576 | + } | ||
| 577 | + dir := config.Cfg.Trace.Dir | ||
| 578 | + if filepath.IsAbs(dir) { | ||
| 579 | + return " | trace: " + dir | ||
| 580 | + } | ||
| 581 | + if acpMode { | ||
| 582 | + return " | trace: <session cwd>/" + dir | ||
| 583 | + } | ||
| 584 | + if abs, err := filepath.Abs(dir); err == nil { | ||
| 585 | + dir = abs | ||
| 586 | + } | ||
| 587 | + return " | trace: " + dir | ||
| 588 | +} | ||
| 589 | + | ||
| 570 | // commandNames is the slash commands' names, for the shadowing check. | 590 | // commandNames is the slash commands' names, for the shadowing check. |
| 571 | func commandNames() []string { | 591 | func commandNames() []string { |
| 572 | var out []string | 592 | var out []string |