| 💾 Saved. d722711 k33g 6h ago | 1 | # Architecture: one loop, two front ends — explanation |
| 2 | |
| 3 | ## What is this about? |
| 4 | |
| 5 | `mm` is deliberately small: an agent loop, a handful of tools, and a configuration file. What makes it worth looking at is how the pieces are cut so that the same loop can be driven from a terminal and from an editor without duplicating anything. |
| 6 | |
| 7 | The entry point, `main.go`, only does wiring: it loads the settings, opens the engine, builds the tool list, prints the banner, and hands everything to one of two front ends. Each concern lives in its own package under `internal/`: |
| 8 | |
| 9 | | Package | Responsibility | |
| 10 | |---------|----------------| |
| 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, and the provider registry. | |
| 13 | | `tools` | The built-in tools the model can call. | |
| 14 | | `fileedit` | Exact-replacement file editing, the rules behind `edit_file`. | |
| 15 | | `skills` | Discovery of markdown procedures and rendering of the `read_skill` catalogue. | |
| 16 | | `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. | |
| 17 | | `detector` | Detection of the same action repeated with the same result. | |
| 18 | | `compact` | Compression of the history into a model-written summary. | |
| 19 | | `spinner` | The "still working" indicator on a terminal. | |
| 20 | | `ui` | Where human-facing output goes, and the event sink the two front ends share. | |
| 21 | | `agent` | The terminal REPL. | |
| 22 | | `acp` | The Agent Client Protocol façade. | |
| 23 | |
| 24 | A draw.io diagram of these packages and their dependencies is kept at [`docs/diagrams/packages.drawio`](../../diagrams/packages.drawio). |
| 25 | |
| 26 | ## Why it is designed this way |
| 27 | |
| 28 | **Tools report failures as text, never as errors.** A command that exits non-zero, a file that does not exist, an edit that is ambiguous: all of it goes back to the model as the tool's output. The model is the one that must react, so it is the one that must read the message. A Go error would end the turn instead. |
| 29 | |
| 30 | **The full history is kept even when a turn fails.** Genkit only returns the last message; the engine returns the whole conversation including tool calls and results. The front ends keep that history after an abort, a watchdog cut or a `maxTurns` overrun, because the commands that already ran are facts the model must not re-run or invent on the next question. Only a question that got no answer at all is dropped. |
| 31 | |
| 32 | **Output is shown before the model sees it.** In the terminal, `bash` echoes the first lines of a command's output before returning it. "Show me that file" means show it; leaving it to the model to summarise was observed to leave the user with nothing. |
| 33 | |
| 34 | **One loop, two front ends.** The terminal REPL and the ACP façade receive exactly the same engine, system prompt and tool list. What differs is where events go. In the terminal, the tools print their own `🛠️` lines and nobody asks permission. In ACP mode, stdout belongs to JSON-RPC, so `main.go` redirects the human output to stderr, disables the spinner, and the façade installs a `ui.Sink` for each turn. The tools check for an active sink: with one, they emit `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` events instead of printing. The `ui` package knows nothing about the protocol; `acp` is the only place that translates those events into `session/update` notifications and permission requests. |
| 35 | |
| 36 | **Permission lives in the sink, not in the tools.** A permission dialog is the main thing an editor adds and the terminal does not have. Putting the policy in the ACP sink keeps the tools identical in both modes; "allow always" is remembered per tool name for the session. |
| 37 | |
| 38 | **Turns are serialised in ACP mode.** The active sink is process-global and the engine was never built for concurrent generations, so a second `session/prompt` waits for the first to finish. The protocol leaves that choice to the agent, and editors do not send overlapping prompts anyway. |
| 39 | |
| 40 | **The loop detector is separate from the history.** It records tool, input and output triples; the same triple three times in a row makes the tool append an instruction to change approach. It is deliberately not reset by `/compact`: forgetting a conversation does not make a repeated command new. |
| 41 | |
| 42 | ## Rejected alternatives |
| 43 | |
| 44 | - **One binary per front end.** Rejected because the point of the exercise is to prove the loop is the same; sharing the wiring in `main.go` is what makes that visible. |
| 45 | - **Printing directly from the ACP façade.** Impossible: the specification forbids anything but protocol messages on stdout. |
| 46 | - **Returning Go errors from tools.** Rejected because it ends the turn instead of letting the model recover. |
| 47 | |
| 48 | ## How it relates to the rest |
| 49 | |
| 50 | - The provider registry that the engine uses is described in [providers](providers.md). |
| 51 | - Why and how the history is shortened is in [context compression](context-compression.md). |
| 52 | - The exact protocol surface is in the [ACP reference](../reference/acp.md); the tools' contract is in the [tools reference](../reference/tools.md). |
| 53 | |
| 54 | ## Package map |
| 55 | |
| 56 | The import graph of every package, with a one-line purpose on each, is kept as a draw.io diagram at [`docs/diagrams/packages.drawio`](../../diagrams/packages.drawio) (opens in diagrams.net or the VS Code Draw.io extension). It is regenerated from `go list` whenever a package is added, removed or re-wired. |