🛟 Updated.
79606ef parent: 8d8d1e8 modified
.tickets/issues/0141-for-the-decision-agent-deactivate-automatically.yaml +2 -2 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 141 |
| 3 | 3 | title: "For the decision agent: deactivate automatically all the tools" |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-24T05:19:33.376Z |
| 9 | -updatedAt: 2026-10-24T05:19:45.037Z | |
| 9 | +updatedAt: 2026-10-25T17:47:45.930Z | |
| 10 | 10 | labels: |
| 11 | 11 | - priority::urgent |
| 12 | 12 | milestone: v1.0.1 |
| @@ -1,12 +1,12 @@ | |||
| 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | id: 141 | 2 | id: 141 |
| 3 | title: "For the decision agent: deactivate automatically all the tools" | 3 | title: "For the decision agent: deactivate automatically all the tools" |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-24T05:19:33.376Z | 8 | createdAt: 2026-10-24T05:19:33.376Z |
| 9 | -updatedAt: 2026-10-24T05:19:45.037Z | 9 | +updatedAt: 2026-10-25T17:47:45.930Z |
| 10 | labels: | 10 | labels: |
| 11 | - priority::urgent | 11 | - priority::urgent |
| 12 | milestone: v1.0.1 | 12 | milestone: v1.0.1 |
added
.tickets/issues/0157-decision-models-show-the-decider-s-answer-in-the.yaml +179 -0 | new file mode 100644 | ||
| @@ -0,0 +1,179 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 157 | |
| 3 | +title: "Decision models: show the decider's answer in the chat (decision.display)" | |
| 4 | +state: closed | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-25T18:05:31.000Z | |
| 9 | +updatedAt: 2026-10-25T18:12:25.212Z | |
| 10 | +labels: | |
| 11 | + - enhancement | |
| 12 | + - kind::feature | |
| 13 | + - hooks | |
| 14 | + - To be tested | |
| 15 | +milestone: v1.0.1 | |
| 16 | +body: | | |
| 17 | + ## Need | |
| 18 | + | |
| 19 | + A chat agent that consults a decider through a `preGeneration` hook (`demos/decision-dnd`, `demos/decision-chat`, `demos/decision-chat-golang`) only showed the chat model's answer: you could not see what the decider answered, nor how confident it was, without reading `.mm/decider/*.json` or a trace. | |
| 20 | + | |
| 21 | + Request: when the decider's file enables it, show the decider's answer as formatted JSON, in another colour, between the user's question and the chat model's answer. | |
| 22 | + | |
| 23 | + ## What was done | |
| 24 | + | |
| 25 | + The chat's mm does not know the decider (the hook runs `mm -p` on it), so the information travels through the JSON of `-p`, then through the hook's reply. | |
| 26 | + | |
| 27 | + 1. **Decider file**: `decision.display: true` (default `false`). | |
| 28 | + | |
| 29 | + ```yaml | |
| 30 | + decision: | |
| 31 | + display: true | |
| 32 | + questions: … | |
| 33 | + ``` | |
| 34 | + | |
| 35 | + 2. **`mm -p` on that decider** adds `"display":true` at the head of the server's JSON (`decide.MarkDisplay`). The rest is unchanged byte for byte; without the key, nothing changes. | |
| 36 | + | |
| 37 | + ```json | |
| 38 | + {"display":true,"model":"hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M","answers":{"spell":{"type":"choice","choice":"fireball",…}},"usage":{…}} | |
| 39 | + ``` | |
| 40 | + | |
| 41 | + 3. **`preGeneration` hook reply**: new field `display` (string). Shown to the user, never sent to the model, never kept in the history; several hooks = several blocks, in hook order; still shown when a later hook stops the turn. The hook's `🪝` line says `+ display (N chars)`. Ignored on the other events. | |
| 42 | + 4. **Display, before the answer** (`ui.Displayer`, `ui.Display`, called by the engine right after the `preGeneration` hooks): | |
| 43 | + - window: a `‣ hook` block in bright blue, lines kept verbatim (JSON keeps its indentation); | |
| 44 | + - terminal: bright blue on a tty, plain in a pipe; | |
| 45 | + - editor (ACP): a fenced block of the agent's message, tagged `json` when the text is JSON. | |
| 46 | + 5. **Demo hooks** updated (see below); enabled in `demos/decision-dnd/spell.yaml`. | |
| 47 | + | |
| 48 | + Rejected: every hook reading the decider's YAML itself (duplicated in each hook, a YAML dependency in each); mm calling the decider itself instead of a hook (re-opens the "triage lives in hooks" decision, much bigger change). | |
| 49 | + | |
| 50 | + ## The change to make in a hook | |
| 51 | + | |
| 52 | + A hook that runs a decider needs **three lines of logic**, nothing else: mm does the marking and the display. | |
| 53 | + | |
| 54 | + 1. Run the decider as before: `mm -config <decider.yaml> -p "<question>"` and parse stdout. | |
| 55 | + 2. If the parsed JSON has `"display": true`, put the text to show in the reply's `display` — the demos use the `answers` object, indented. Without the flag, do not add the field: the decider's author decides, in the decider's file. | |
| 56 | + 3. Keep `context` as it was: it is still what the chat model reads. `display` never reaches the model. | |
| 57 | + | |
| 58 | + Reply before: | |
| 59 | + | |
| 60 | + ```json | |
| 61 | + {"context":"spell: fireball (confidence 0.44)"} | |
| 62 | + ``` | |
| 63 | + | |
| 64 | + Reply after (decider with `display: true`): | |
| 65 | + | |
| 66 | + ```json | |
| 67 | + {"context":"spell: fireball (confidence 0.44)","display":"{\n \"spell\": {\n \"type\": \"choice\", …\n }\n}"} | |
| 68 | + ``` | |
| 69 | + | |
| 70 | + Node (`demos/decision-chat/hooks/ask-decider.js`), replacing `console.log(JSON.stringify({ context }))`: | |
| 71 | + | |
| 72 | + ```js | |
| 73 | + const reply = { context }; | |
| 74 | + if (payload.display === true) { | |
| 75 | + reply.display = JSON.stringify(payload.answers, null, 2); | |
| 76 | + } | |
| 77 | + console.log(JSON.stringify(reply)); | |
| 78 | + ``` | |
| 79 | + | |
| 80 | + Go (`demos/decision-dnd/hooks/ask-decider/main.go`, same in `demos/decision-chat-golang`): | |
| 81 | + | |
| 82 | + ```go | |
| 83 | + fields := map[string]string{"context": context} | |
| 84 | + if display := displayFor(payload); display != "" { | |
| 85 | + fields["display"] = display | |
| 86 | + } | |
| 87 | + reply, _ := json.Marshal(fields) | |
| 88 | + | |
| 89 | + // displayFor returns the decider's answers, indented, when its file asked | |
| 90 | + // for them to be shown; "" otherwise. | |
| 91 | + func displayFor(payload []byte) string { | |
| 92 | + var envelope struct { | |
| 93 | + Display bool `json:"display"` | |
| 94 | + Answers json.RawMessage `json:"answers"` | |
| 95 | + } | |
| 96 | + if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 { | |
| 97 | + return "" | |
| 98 | + } | |
| 99 | + var indented bytes.Buffer | |
| 100 | + if json.Indent(&indented, envelope.Answers, "", " ") != nil { | |
| 101 | + return "" | |
| 102 | + } | |
| 103 | + return indented.String() | |
| 104 | + } | |
| 105 | + ``` | |
| 106 | + | |
| 107 | + Notes for hook authors: | |
| 108 | + | |
| 109 | + - Use `json.Indent` / a raw object rather than decoding into a map: a Go map would reorder the questions. | |
| 110 | + - `display` can be any text, not only a decider's JSON: any `preGeneration` hook can show the user something without the model reading it. | |
| 111 | + - Old hooks keep working unchanged: they just never show anything. Old deciders (no `display`) print exactly the same JSON as before. | |
| 112 | + - The other decision demos (`decision-commands`, `decision-ask-check`, `decision-tool-errors`, `decision-injection`) use other events, where `display` is ignored; they were not changed. | |
| 113 | + | |
| 114 | + ## Verified | |
| 115 | + | |
| 116 | + - `go vet ./...` and `go test ./...` green; tests added in config, decide, `-p`, hooks, engine (shown before the answer, never sent to the model), ui, window, ACP and both Go hooks; the node hook was checked with a fake `mm`. | |
| 117 | + - Live (llmman): `mm -config dungeon_master.yaml -p "Six kobolds…"` prints the indented answers before the answer; the window, driven through a pseudo-terminal, draws `‣ hook` + JSON in bright blue. | |
| 118 | + - **Not verified**: Zed (ACP) — only the unit test of the fenced block covers it. | |
| 119 | + - Quality gate not run (qlty missing from the sandbox). | |
| 120 | + | |
| 121 | + ## Noticed while testing (out of scope) | |
| 122 | + | |
| 123 | + The decider answered `fireball` (confidence 0.44) but the Dungeon Master (Qwen3.5-4B) wrote `spell: sleep (confidence 0.85)`: the chat model makes up its own decision line. That is the demo's prompt; showing the decider's answer makes the gap visible. | |
| 124 | + | |
| 125 | + ## Docs | |
| 126 | + | |
| 127 | + EN + FR: `reference/configuration.md` (`decision.display`), `reference/hooks.md` (`display` field, effect on `preGeneration`), `reference/cli.md` (`-p`), `how-to/ask-a-decision-model.md`, `demos/decision-dnd` README. `docs/diagrams/packages.drawio`: new edge `ui` → `spinner`. | |
| 128 | +tasks: | |
| 129 | + - id: 1 | |
| 130 | + title: decision.display in the decider file; -p marks its JSON with "display":true | |
| 131 | + state: closed | |
| 132 | + priority: none | |
| 133 | + author: | |
| 134 | + name: k33g | |
| 135 | + email: ph.charriere@gmail.com | |
| 136 | + createdAt: 2026-10-25T18:05:31.000Z | |
| 137 | + - id: 2 | |
| 138 | + title: Hook reply field `display` (preGeneration), shown before the answer, never sent to the model | |
| 139 | + state: closed | |
| 140 | + priority: none | |
| 141 | + author: | |
| 142 | + name: k33g | |
| 143 | + email: ph.charriere@gmail.com | |
| 144 | + createdAt: 2026-10-25T18:05:31.000Z | |
| 145 | + - id: 3 | |
| 146 | + title: "Front ends: window (blue `hook` block), terminal (blue), editor (fenced json)" | |
| 147 | + state: closed | |
| 148 | + priority: none | |
| 149 | + author: | |
| 150 | + name: k33g | |
| 151 | + email: ph.charriere@gmail.com | |
| 152 | + createdAt: 2026-10-25T18:05:31.000Z | |
| 153 | + - id: 4 | |
| 154 | + title: Demo hooks pass the answers on (decision-dnd, decision-chat-golang, decision-chat); on in decision-dnd | |
| 155 | + state: closed | |
| 156 | + priority: none | |
| 157 | + author: | |
| 158 | + name: k33g | |
| 159 | + email: ph.charriere@gmail.com | |
| 160 | + createdAt: 2026-10-25T18:05:31.000Z | |
| 161 | + - id: 5 | |
| 162 | + title: Tests in every layer + docs EN/FR | |
| 163 | + state: closed | |
| 164 | + priority: none | |
| 165 | + author: | |
| 166 | + name: k33g | |
| 167 | + email: ph.charriere@gmail.com | |
| 168 | + createdAt: 2026-10-25T18:05:31.000Z | |
| 169 | + - id: 6 | |
| 170 | + title: Check in Zed (ACP) that the json block arrives before the answer | |
| 171 | + state: closed | |
| 172 | + priority: none | |
| 173 | + author: | |
| 174 | + name: k33g | |
| 175 | + email: ph.charriere@gmail.com | |
| 176 | + createdAt: 2026-10-25T18:05:31.000Z | |
| 177 | + updatedAt: 2026-10-25T18:12:22.755Z | |
| 178 | + closedAt: 2026-10-25T18:12:22.755Z | |
| 179 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,179 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 157 | ||
| 3 | +title: "Decision models: show the decider's answer in the chat (decision.display)" | ||
| 4 | +state: closed | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-25T18:05:31.000Z | ||
| 9 | +updatedAt: 2026-10-25T18:12:25.212Z | ||
| 10 | +labels: | ||
| 11 | + - enhancement | ||
| 12 | + - kind::feature | ||
| 13 | + - hooks | ||
| 14 | + - To be tested | ||
| 15 | +milestone: v1.0.1 | ||
| 16 | +body: | | ||
| 17 | + ## Need | ||
| 18 | + | ||
| 19 | + A chat agent that consults a decider through a `preGeneration` hook (`demos/decision-dnd`, `demos/decision-chat`, `demos/decision-chat-golang`) only showed the chat model's answer: you could not see what the decider answered, nor how confident it was, without reading `.mm/decider/*.json` or a trace. | ||
| 20 | + | ||
| 21 | + Request: when the decider's file enables it, show the decider's answer as formatted JSON, in another colour, between the user's question and the chat model's answer. | ||
| 22 | + | ||
| 23 | + ## What was done | ||
| 24 | + | ||
| 25 | + The chat's mm does not know the decider (the hook runs `mm -p` on it), so the information travels through the JSON of `-p`, then through the hook's reply. | ||
| 26 | + | ||
| 27 | + 1. **Decider file**: `decision.display: true` (default `false`). | ||
| 28 | + | ||
| 29 | + ```yaml | ||
| 30 | + decision: | ||
| 31 | + display: true | ||
| 32 | + questions: … | ||
| 33 | + ``` | ||
| 34 | + | ||
| 35 | + 2. **`mm -p` on that decider** adds `"display":true` at the head of the server's JSON (`decide.MarkDisplay`). The rest is unchanged byte for byte; without the key, nothing changes. | ||
| 36 | + | ||
| 37 | + ```json | ||
| 38 | + {"display":true,"model":"hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M","answers":{"spell":{"type":"choice","choice":"fireball",…}},"usage":{…}} | ||
| 39 | + ``` | ||
| 40 | + | ||
| 41 | + 3. **`preGeneration` hook reply**: new field `display` (string). Shown to the user, never sent to the model, never kept in the history; several hooks = several blocks, in hook order; still shown when a later hook stops the turn. The hook's `🪝` line says `+ display (N chars)`. Ignored on the other events. | ||
| 42 | + 4. **Display, before the answer** (`ui.Displayer`, `ui.Display`, called by the engine right after the `preGeneration` hooks): | ||
| 43 | + - window: a `‣ hook` block in bright blue, lines kept verbatim (JSON keeps its indentation); | ||
| 44 | + - terminal: bright blue on a tty, plain in a pipe; | ||
| 45 | + - editor (ACP): a fenced block of the agent's message, tagged `json` when the text is JSON. | ||
| 46 | + 5. **Demo hooks** updated (see below); enabled in `demos/decision-dnd/spell.yaml`. | ||
| 47 | + | ||
| 48 | + Rejected: every hook reading the decider's YAML itself (duplicated in each hook, a YAML dependency in each); mm calling the decider itself instead of a hook (re-opens the "triage lives in hooks" decision, much bigger change). | ||
| 49 | + | ||
| 50 | + ## The change to make in a hook | ||
| 51 | + | ||
| 52 | + A hook that runs a decider needs **three lines of logic**, nothing else: mm does the marking and the display. | ||
| 53 | + | ||
| 54 | + 1. Run the decider as before: `mm -config <decider.yaml> -p "<question>"` and parse stdout. | ||
| 55 | + 2. If the parsed JSON has `"display": true`, put the text to show in the reply's `display` — the demos use the `answers` object, indented. Without the flag, do not add the field: the decider's author decides, in the decider's file. | ||
| 56 | + 3. Keep `context` as it was: it is still what the chat model reads. `display` never reaches the model. | ||
| 57 | + | ||
| 58 | + Reply before: | ||
| 59 | + | ||
| 60 | + ```json | ||
| 61 | + {"context":"spell: fireball (confidence 0.44)"} | ||
| 62 | + ``` | ||
| 63 | + | ||
| 64 | + Reply after (decider with `display: true`): | ||
| 65 | + | ||
| 66 | + ```json | ||
| 67 | + {"context":"spell: fireball (confidence 0.44)","display":"{\n \"spell\": {\n \"type\": \"choice\", …\n }\n}"} | ||
| 68 | + ``` | ||
| 69 | + | ||
| 70 | + Node (`demos/decision-chat/hooks/ask-decider.js`), replacing `console.log(JSON.stringify({ context }))`: | ||
| 71 | + | ||
| 72 | + ```js | ||
| 73 | + const reply = { context }; | ||
| 74 | + if (payload.display === true) { | ||
| 75 | + reply.display = JSON.stringify(payload.answers, null, 2); | ||
| 76 | + } | ||
| 77 | + console.log(JSON.stringify(reply)); | ||
| 78 | + ``` | ||
| 79 | + | ||
| 80 | + Go (`demos/decision-dnd/hooks/ask-decider/main.go`, same in `demos/decision-chat-golang`): | ||
| 81 | + | ||
| 82 | + ```go | ||
| 83 | + fields := map[string]string{"context": context} | ||
| 84 | + if display := displayFor(payload); display != "" { | ||
| 85 | + fields["display"] = display | ||
| 86 | + } | ||
| 87 | + reply, _ := json.Marshal(fields) | ||
| 88 | + | ||
| 89 | + // displayFor returns the decider's answers, indented, when its file asked | ||
| 90 | + // for them to be shown; "" otherwise. | ||
| 91 | + func displayFor(payload []byte) string { | ||
| 92 | + var envelope struct { | ||
| 93 | + Display bool `json:"display"` | ||
| 94 | + Answers json.RawMessage `json:"answers"` | ||
| 95 | + } | ||
| 96 | + if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 { | ||
| 97 | + return "" | ||
| 98 | + } | ||
| 99 | + var indented bytes.Buffer | ||
| 100 | + if json.Indent(&indented, envelope.Answers, "", " ") != nil { | ||
| 101 | + return "" | ||
| 102 | + } | ||
| 103 | + return indented.String() | ||
| 104 | + } | ||
| 105 | + ``` | ||
| 106 | + | ||
| 107 | + Notes for hook authors: | ||
| 108 | + | ||
| 109 | + - Use `json.Indent` / a raw object rather than decoding into a map: a Go map would reorder the questions. | ||
| 110 | + - `display` can be any text, not only a decider's JSON: any `preGeneration` hook can show the user something without the model reading it. | ||
| 111 | + - Old hooks keep working unchanged: they just never show anything. Old deciders (no `display`) print exactly the same JSON as before. | ||
| 112 | + - The other decision demos (`decision-commands`, `decision-ask-check`, `decision-tool-errors`, `decision-injection`) use other events, where `display` is ignored; they were not changed. | ||
| 113 | + | ||
| 114 | + ## Verified | ||
| 115 | + | ||
| 116 | + - `go vet ./...` and `go test ./...` green; tests added in config, decide, `-p`, hooks, engine (shown before the answer, never sent to the model), ui, window, ACP and both Go hooks; the node hook was checked with a fake `mm`. | ||
| 117 | + - Live (llmman): `mm -config dungeon_master.yaml -p "Six kobolds…"` prints the indented answers before the answer; the window, driven through a pseudo-terminal, draws `‣ hook` + JSON in bright blue. | ||
| 118 | + - **Not verified**: Zed (ACP) — only the unit test of the fenced block covers it. | ||
| 119 | + - Quality gate not run (qlty missing from the sandbox). | ||
| 120 | + | ||
| 121 | + ## Noticed while testing (out of scope) | ||
| 122 | + | ||
| 123 | + The decider answered `fireball` (confidence 0.44) but the Dungeon Master (Qwen3.5-4B) wrote `spell: sleep (confidence 0.85)`: the chat model makes up its own decision line. That is the demo's prompt; showing the decider's answer makes the gap visible. | ||
| 124 | + | ||
| 125 | + ## Docs | ||
| 126 | + | ||
| 127 | + EN + FR: `reference/configuration.md` (`decision.display`), `reference/hooks.md` (`display` field, effect on `preGeneration`), `reference/cli.md` (`-p`), `how-to/ask-a-decision-model.md`, `demos/decision-dnd` README. `docs/diagrams/packages.drawio`: new edge `ui` → `spinner`. | ||
| 128 | +tasks: | ||
| 129 | + - id: 1 | ||
| 130 | + title: decision.display in the decider file; -p marks its JSON with "display":true | ||
| 131 | + state: closed | ||
| 132 | + priority: none | ||
| 133 | + author: | ||
| 134 | + name: k33g | ||
| 135 | + email: ph.charriere@gmail.com | ||
| 136 | + createdAt: 2026-10-25T18:05:31.000Z | ||
| 137 | + - id: 2 | ||
| 138 | + title: Hook reply field `display` (preGeneration), shown before the answer, never sent to the model | ||
| 139 | + state: closed | ||
| 140 | + priority: none | ||
| 141 | + author: | ||
| 142 | + name: k33g | ||
| 143 | + email: ph.charriere@gmail.com | ||
| 144 | + createdAt: 2026-10-25T18:05:31.000Z | ||
| 145 | + - id: 3 | ||
| 146 | + title: "Front ends: window (blue `hook` block), terminal (blue), editor (fenced json)" | ||
| 147 | + state: closed | ||
| 148 | + priority: none | ||
| 149 | + author: | ||
| 150 | + name: k33g | ||
| 151 | + email: ph.charriere@gmail.com | ||
| 152 | + createdAt: 2026-10-25T18:05:31.000Z | ||
| 153 | + - id: 4 | ||
| 154 | + title: Demo hooks pass the answers on (decision-dnd, decision-chat-golang, decision-chat); on in decision-dnd | ||
| 155 | + state: closed | ||
| 156 | + priority: none | ||
| 157 | + author: | ||
| 158 | + name: k33g | ||
| 159 | + email: ph.charriere@gmail.com | ||
| 160 | + createdAt: 2026-10-25T18:05:31.000Z | ||
| 161 | + - id: 5 | ||
| 162 | + title: Tests in every layer + docs EN/FR | ||
| 163 | + state: closed | ||
| 164 | + priority: none | ||
| 165 | + author: | ||
| 166 | + name: k33g | ||
| 167 | + email: ph.charriere@gmail.com | ||
| 168 | + createdAt: 2026-10-25T18:05:31.000Z | ||
| 169 | + - id: 6 | ||
| 170 | + title: Check in Zed (ACP) that the json block arrives before the answer | ||
| 171 | + state: closed | ||
| 172 | + priority: none | ||
| 173 | + author: | ||
| 174 | + name: k33g | ||
| 175 | + email: ph.charriere@gmail.com | ||
| 176 | + createdAt: 2026-10-25T18:05:31.000Z | ||
| 177 | + updatedAt: 2026-10-25T18:12:22.755Z | ||
| 178 | + closedAt: 2026-10-25T18:12:22.755Z | ||
| 179 | +comments: [] | ||
modified
decision_wiring.go +5 -1 | @@ -22,7 +22,11 @@ func runDecisionPrompt(ctx context.Context, e *engine.Engine, prompt string, out | ||
| 22 | 22 | fail("[decision: %v]", err) |
| 23 | 23 | return 1 |
| 24 | 24 | } |
| 25 | - _, _ = fmt.Fprintln(out, string(res.Raw)) | |
| 25 | + raw := res.Raw | |
| 26 | + if config.Cfg.Decision.Display { | |
| 27 | + raw = decide.MarkDisplay(raw) | |
| 28 | + } | |
| 29 | + _, _ = fmt.Fprintln(out, string(raw)) | |
| 26 | 30 | return 0 |
| 27 | 31 | } |
| 28 | 32 | |
| @@ -22,7 +22,11 @@ func runDecisionPrompt(ctx context.Context, e *engine.Engine, prompt string, out | |||
| 22 | fail("[decision: %v]", err) | 22 | fail("[decision: %v]", err) |
| 23 | return 1 | 23 | return 1 |
| 24 | } | 24 | } |
| 25 | - _, _ = fmt.Fprintln(out, string(res.Raw)) | 25 | + raw := res.Raw |
| 26 | + if config.Cfg.Decision.Display { | ||
| 27 | + raw = decide.MarkDisplay(raw) | ||
| 28 | + } | ||
| 29 | + _, _ = fmt.Fprintln(out, string(raw)) | ||
| 26 | return 0 | 30 | return 0 |
| 27 | } | 31 | } |
| 28 | 32 | ||
modified
decision_wiring_test.go +19 -0 | @@ -56,6 +56,25 @@ func TestDecisionPromptPrintsTheServersJSONAlone(t *testing.T) { | ||
| 56 | 56 | } |
| 57 | 57 | } |
| 58 | 58 | |
| 59 | +// decision.display: true marks the JSON for the hook that runs -p; the | |
| 60 | +// server's answer follows unchanged. | |
| 61 | +func TestDecisionPromptMarksTheJSONWhenDisplayIsOn(t *testing.T) { | |
| 62 | + deciderConfig(t, deciderServer(t, http.StatusOK, deciderAnswer)) | |
| 63 | + config.Cfg.Decision.Display = true | |
| 64 | + e, err := engine.New(context.Background()) | |
| 65 | + if err != nil { | |
| 66 | + t.Fatal(err) | |
| 67 | + } | |
| 68 | + var out bytes.Buffer | |
| 69 | + if code := runDecisionPrompt(context.Background(), e, "rm -rf ~/data", &out); code != 0 { | |
| 70 | + t.Fatalf("exit %d", code) | |
| 71 | + } | |
| 72 | + want := `{"display":true,` + strings.TrimPrefix(deciderAnswer, "{") + "\n" | |
| 73 | + if out.String() != want { | |
| 74 | + t.Errorf("stdout = %q, want %q", out.String(), want) | |
| 75 | + } | |
| 76 | +} | |
| 77 | + | |
| 59 | 78 | func TestAFailedDecisionPromptExitsOneWithTheReasonOnStderr(t *testing.T) { |
| 60 | 79 | deciderConfig(t, deciderServer(t, http.StatusNotImplemented, `{"error":{"message":"This model is not a decision model"}}`)) |
| 61 | 80 | e, err := engine.New(context.Background()) |
| @@ -56,6 +56,25 @@ func TestDecisionPromptPrintsTheServersJSONAlone(t *testing.T) { | |||
| 56 | } | 56 | } |
| 57 | } | 57 | } |
| 58 | 58 | ||
| 59 | +// decision.display: true marks the JSON for the hook that runs -p; the | ||
| 60 | +// server's answer follows unchanged. | ||
| 61 | +func TestDecisionPromptMarksTheJSONWhenDisplayIsOn(t *testing.T) { | ||
| 62 | + deciderConfig(t, deciderServer(t, http.StatusOK, deciderAnswer)) | ||
| 63 | + config.Cfg.Decision.Display = true | ||
| 64 | + e, err := engine.New(context.Background()) | ||
| 65 | + if err != nil { | ||
| 66 | + t.Fatal(err) | ||
| 67 | + } | ||
| 68 | + var out bytes.Buffer | ||
| 69 | + if code := runDecisionPrompt(context.Background(), e, "rm -rf ~/data", &out); code != 0 { | ||
| 70 | + t.Fatalf("exit %d", code) | ||
| 71 | + } | ||
| 72 | + want := `{"display":true,` + strings.TrimPrefix(deciderAnswer, "{") + "\n" | ||
| 73 | + if out.String() != want { | ||
| 74 | + t.Errorf("stdout = %q, want %q", out.String(), want) | ||
| 75 | + } | ||
| 76 | +} | ||
| 77 | + | ||
| 59 | func TestAFailedDecisionPromptExitsOneWithTheReasonOnStderr(t *testing.T) { | 78 | func TestAFailedDecisionPromptExitsOneWithTheReasonOnStderr(t *testing.T) { |
| 60 | deciderConfig(t, deciderServer(t, http.StatusNotImplemented, `{"error":{"message":"This model is not a decision model"}}`)) | 79 | deciderConfig(t, deciderServer(t, http.StatusNotImplemented, `{"error":{"message":"This model is not a decision model"}}`)) |
| 61 | e, err := engine.New(context.Background()) | 80 | e, err := engine.New(context.Background()) |
modified
demos/decision-chat-golang/hooks/ask-decider/main.go +28 -1 | @@ -12,6 +12,11 @@ | ||
| 12 | 12 | // order, so changing the decider's questions needs no change here. Set |
| 13 | 13 | // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines. |
| 14 | 14 | // |
| 15 | +// When the decider's file sets `decision.display: true`, `mm -p` marks its | |
| 16 | +// JSON with `"display": true`, and the reply also carries `display`: the | |
| 17 | +// answers, indented, which mm shows the user (in its own colour) before the | |
| 18 | +// chat model answers — never to the model. | |
| 19 | +// | |
| 15 | 20 | // The decider file is the first argument, relative to the folder of the agent |
| 16 | 21 | // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml: |
| 17 | 22 | // |
| @@ -86,7 +91,11 @@ func main() { | ||
| 86 | 91 | return |
| 87 | 92 | } |
| 88 | 93 | rec.Context = context |
| 89 | - reply, _ := json.Marshal(map[string]string{"context": context}) | |
| 94 | + fields := map[string]string{"context": context} | |
| 95 | + if display := displayFor(payload); display != "" { | |
| 96 | + fields["display"] = display | |
| 97 | + } | |
| 98 | + reply, _ := json.Marshal(fields) | |
| 90 | 99 | fmt.Println(string(reply)) |
| 91 | 100 | } |
| 92 | 101 | |
| @@ -152,6 +161,24 @@ func contextFor(payload []byte, raw bool) (string, error) { | ||
| 152 | 161 | return strings.Join(lines, "\n"), nil |
| 153 | 162 | } |
| 154 | 163 | |
| 164 | +// displayFor returns the decider's answers, indented, when its file asked | |
| 165 | +// for them to be shown (`"display": true` in the JSON of `mm -p`); "" | |
| 166 | +// otherwise, or when the payload cannot be read. | |
| 167 | +func displayFor(payload []byte) string { | |
| 168 | + var envelope struct { | |
| 169 | + Display bool `json:"display"` | |
| 170 | + Answers json.RawMessage `json:"answers"` | |
| 171 | + } | |
| 172 | + if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 { | |
| 173 | + return "" | |
| 174 | + } | |
| 175 | + var indented bytes.Buffer | |
| 176 | + if json.Indent(&indented, envelope.Answers, "", " ") != nil { | |
| 177 | + return "" | |
| 178 | + } | |
| 179 | + return indented.String() | |
| 180 | +} | |
| 181 | + | |
| 155 | 182 | // inOrder decodes the answers object keeping the order of its keys, which is |
| 156 | 183 | // the order of the decider's questions (a Go map would lose it). |
| 157 | 184 | func inOrder(object json.RawMessage) ([]named, error) { |
| @@ -12,6 +12,11 @@ | |||
| 12 | // order, so changing the decider's questions needs no change here. Set | 12 | // order, so changing the decider's questions needs no change here. Set |
| 13 | // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines. | 13 | // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines. |
| 14 | // | 14 | // |
| 15 | +// When the decider's file sets `decision.display: true`, `mm -p` marks its | ||
| 16 | +// JSON with `"display": true`, and the reply also carries `display`: the | ||
| 17 | +// answers, indented, which mm shows the user (in its own colour) before the | ||
| 18 | +// chat model answers — never to the model. | ||
| 19 | +// | ||
| 15 | // The decider file is the first argument, relative to the folder of the agent | 20 | // The decider file is the first argument, relative to the folder of the agent |
| 16 | // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml: | 21 | // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml: |
| 17 | // | 22 | // |
| @@ -86,7 +91,11 @@ func main() { | |||
| 86 | return | 91 | return |
| 87 | } | 92 | } |
| 88 | rec.Context = context | 93 | rec.Context = context |
| 89 | - reply, _ := json.Marshal(map[string]string{"context": context}) | 94 | + fields := map[string]string{"context": context} |
| 95 | + if display := displayFor(payload); display != "" { | ||
| 96 | + fields["display"] = display | ||
| 97 | + } | ||
| 98 | + reply, _ := json.Marshal(fields) | ||
| 90 | fmt.Println(string(reply)) | 99 | fmt.Println(string(reply)) |
| 91 | } | 100 | } |
| 92 | 101 | ||
| @@ -152,6 +161,24 @@ func contextFor(payload []byte, raw bool) (string, error) { | |||
| 152 | return strings.Join(lines, "\n"), nil | 161 | return strings.Join(lines, "\n"), nil |
| 153 | } | 162 | } |
| 154 | 163 | ||
| 164 | +// displayFor returns the decider's answers, indented, when its file asked | ||
| 165 | +// for them to be shown (`"display": true` in the JSON of `mm -p`); "" | ||
| 166 | +// otherwise, or when the payload cannot be read. | ||
| 167 | +func displayFor(payload []byte) string { | ||
| 168 | + var envelope struct { | ||
| 169 | + Display bool `json:"display"` | ||
| 170 | + Answers json.RawMessage `json:"answers"` | ||
| 171 | + } | ||
| 172 | + if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 { | ||
| 173 | + return "" | ||
| 174 | + } | ||
| 175 | + var indented bytes.Buffer | ||
| 176 | + if json.Indent(&indented, envelope.Answers, "", " ") != nil { | ||
| 177 | + return "" | ||
| 178 | + } | ||
| 179 | + return indented.String() | ||
| 180 | +} | ||
| 181 | + | ||
| 155 | // inOrder decodes the answers object keeping the order of its keys, which is | 182 | // inOrder decodes the answers object keeping the order of its keys, which is |
| 156 | // the order of the decider's questions (a Go map would lose it). | 183 | // the order of the decider's questions (a Go map would lose it). |
| 157 | func inOrder(object json.RawMessage) ([]named, error) { | 184 | func inOrder(object json.RawMessage) ([]named, error) { |
modified
demos/decision-chat-golang/hooks/ask-decider/main_test.go +17 -0 | @@ -59,3 +59,20 @@ func TestDeciderFileDefaultsToAgentDeciderYaml(t *testing.T) { | ||
| 59 | 59 | t.Fatalf("one argument: got %q", got) |
| 60 | 60 | } |
| 61 | 61 | } |
| 62 | + | |
| 63 | +func TestDisplayForIndentsTheAnswersWhenTheDeciderAsks(t *testing.T) { | |
| 64 | + marked := `{"display":true,"model":"d1","answers":{"spell":{"type":"choice","choice":"sleep","confidence":0.8}}}` | |
| 65 | + want := "{\n \"spell\": {\n \"type\": \"choice\",\n \"choice\": \"sleep\",\n \"confidence\": 0.8\n }\n}" | |
| 66 | + if got := displayFor([]byte(marked)); got != want { | |
| 67 | + t.Fatalf("got\n%s\nwant\n%s", got, want) | |
| 68 | + } | |
| 69 | +} | |
| 70 | + | |
| 71 | +func TestDisplayForIsEmptyWithoutTheFlag(t *testing.T) { | |
| 72 | + if got := displayFor([]byte(payload)); got != "" { | |
| 73 | + t.Fatalf("got %q, want nothing to show", got) | |
| 74 | + } | |
| 75 | + if got := displayFor([]byte("not json")); got != "" { | |
| 76 | + t.Fatalf("got %q", got) | |
| 77 | + } | |
| 78 | +} | |
| @@ -59,3 +59,20 @@ func TestDeciderFileDefaultsToAgentDeciderYaml(t *testing.T) { | |||
| 59 | t.Fatalf("one argument: got %q", got) | 59 | t.Fatalf("one argument: got %q", got) |
| 60 | } | 60 | } |
| 61 | } | 61 | } |
| 62 | + | ||
| 63 | +func TestDisplayForIndentsTheAnswersWhenTheDeciderAsks(t *testing.T) { | ||
| 64 | + marked := `{"display":true,"model":"d1","answers":{"spell":{"type":"choice","choice":"sleep","confidence":0.8}}}` | ||
| 65 | + want := "{\n \"spell\": {\n \"type\": \"choice\",\n \"choice\": \"sleep\",\n \"confidence\": 0.8\n }\n}" | ||
| 66 | + if got := displayFor([]byte(marked)); got != want { | ||
| 67 | + t.Fatalf("got\n%s\nwant\n%s", got, want) | ||
| 68 | + } | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +func TestDisplayForIsEmptyWithoutTheFlag(t *testing.T) { | ||
| 72 | + if got := displayFor([]byte(payload)); got != "" { | ||
| 73 | + t.Fatalf("got %q, want nothing to show", got) | ||
| 74 | + } | ||
| 75 | + if got := displayFor([]byte("not json")); got != "" { | ||
| 76 | + t.Fatalf("got %q", got) | ||
| 77 | + } | ||
| 78 | +} | ||
modified
demos/decision-chat/hooks/ask-decider.js +10 -1 | @@ -11,6 +11,11 @@ | ||
| 11 | 11 | // changing agent.decider.yaml needs no change here. Set DECIDER_RAW=1 to hand |
| 12 | 12 | // over the server's JSON payload instead of the lines. |
| 13 | 13 | // |
| 14 | +// When agent.decider.yaml sets `decision.display: true`, `mm -p` marks its | |
| 15 | +// JSON with `"display": true`, and the reply also carries `display`: the | |
| 16 | +// answers, indented, which mm shows the user (in its own colour) before the | |
| 17 | +// chat model answers — never to the model. | |
| 18 | +// | |
| 14 | 19 | // MM_BIN is the running mm (set by mm for its hooks). A decider that fails |
| 15 | 20 | // adds nothing: the chat model reads the question alone. |
| 16 | 21 | |
| @@ -49,4 +54,8 @@ try { | ||
| 49 | 54 | const context = process.env.DECIDER_RAW |
| 50 | 55 | ? JSON.stringify(payload.answers) |
| 51 | 56 | : Object.entries(payload.answers).map(([name, answer]) => line(name, answer)).join("\n"); |
| 52 | -console.log(JSON.stringify({ context })); | |
| 57 | +const reply = { context }; | |
| 58 | +if (payload.display === true) { | |
| 59 | + reply.display = JSON.stringify(payload.answers, null, 2); | |
| 60 | +} | |
| 61 | +console.log(JSON.stringify(reply)); | |
| @@ -11,6 +11,11 @@ | |||
| 11 | // changing agent.decider.yaml needs no change here. Set DECIDER_RAW=1 to hand | 11 | // changing agent.decider.yaml needs no change here. Set DECIDER_RAW=1 to hand |
| 12 | // over the server's JSON payload instead of the lines. | 12 | // over the server's JSON payload instead of the lines. |
| 13 | // | 13 | // |
| 14 | +// When agent.decider.yaml sets `decision.display: true`, `mm -p` marks its | ||
| 15 | +// JSON with `"display": true`, and the reply also carries `display`: the | ||
| 16 | +// answers, indented, which mm shows the user (in its own colour) before the | ||
| 17 | +// chat model answers — never to the model. | ||
| 18 | +// | ||
| 14 | // MM_BIN is the running mm (set by mm for its hooks). A decider that fails | 19 | // MM_BIN is the running mm (set by mm for its hooks). A decider that fails |
| 15 | // adds nothing: the chat model reads the question alone. | 20 | // adds nothing: the chat model reads the question alone. |
| 16 | 21 | ||
| @@ -49,4 +54,8 @@ try { | |||
| 49 | const context = process.env.DECIDER_RAW | 54 | const context = process.env.DECIDER_RAW |
| 50 | ? JSON.stringify(payload.answers) | 55 | ? JSON.stringify(payload.answers) |
| 51 | : Object.entries(payload.answers).map(([name, answer]) => line(name, answer)).join("\n"); | 56 | : Object.entries(payload.answers).map(([name, answer]) => line(name, answer)).join("\n"); |
| 52 | -console.log(JSON.stringify({ context })); | 57 | +const reply = { context }; |
| 58 | +if (payload.display === true) { | ||
| 59 | + reply.display = JSON.stringify(payload.answers, null, 2); | ||
| 60 | +} | ||
| 61 | +console.log(JSON.stringify(reply)); | ||
modified
demos/decision-dnd/README.en.md +2 -1 | @@ -33,7 +33,8 @@ mm -config spell.yaml -p "An ogre is raising its club to hit me right now." # | ||
| 33 | 33 | |
| 34 | 34 | ## See what the decider answered |
| 35 | 35 | |
| 36 | -- **The decider alone**: `mm -config spell.yaml -p "…"` prints the server's JSON (add `| jq`). | |
| 36 | +- **In the chat, before every answer**: `spell.yaml` sets `display: true`, so the decider's answers appear as indented JSON, in blue under `‣ hook`, between your question and the Dungeon Master's answer (a fenced block in an editor). Remove the line to hide them. | |
| 37 | +- **The decider alone**: `mm -config spell.yaml -p "…"` prints the server's JSON (add `| jq`), headed by `"display":true`. | |
| 37 | 38 | - **One JSON file per question**: `DECIDER_DUMP=.mm/decider` in the hook's command (on in `dungeon_master.yaml`; remove it to stop). Each question leaves `.mm/decider/<time>-spell.json` with the question, the decider's whole response, the line handed to the Dungeon Master — or the error when the decider failed. `.mm/` is git-ignored. |
| 38 | 39 | - **What the Dungeon Master read**: `AGENT_TRACE=true mm dungeon_master.yaml`, then the last user message of `.mm/traces/…request.json`. |
| 39 | 40 | |
| @@ -33,7 +33,8 @@ mm -config spell.yaml -p "An ogre is raising its club to hit me right now." # | |||
| 33 | 33 | ||
| 34 | ## See what the decider answered | 34 | ## See what the decider answered |
| 35 | 35 | ||
| 36 | -- **The decider alone**: `mm -config spell.yaml -p "…"` prints the server's JSON (add `| jq`). | 36 | +- **In the chat, before every answer**: `spell.yaml` sets `display: true`, so the decider's answers appear as indented JSON, in blue under `‣ hook`, between your question and the Dungeon Master's answer (a fenced block in an editor). Remove the line to hide them. |
| 37 | +- **The decider alone**: `mm -config spell.yaml -p "…"` prints the server's JSON (add `| jq`), headed by `"display":true`. | ||
| 37 | - **One JSON file per question**: `DECIDER_DUMP=.mm/decider` in the hook's command (on in `dungeon_master.yaml`; remove it to stop). Each question leaves `.mm/decider/<time>-spell.json` with the question, the decider's whole response, the line handed to the Dungeon Master — or the error when the decider failed. `.mm/` is git-ignored. | 38 | - **One JSON file per question**: `DECIDER_DUMP=.mm/decider` in the hook's command (on in `dungeon_master.yaml`; remove it to stop). Each question leaves `.mm/decider/<time>-spell.json` with the question, the decider's whole response, the line handed to the Dungeon Master — or the error when the decider failed. `.mm/` is git-ignored. |
| 38 | - **What the Dungeon Master read**: `AGENT_TRACE=true mm dungeon_master.yaml`, then the last user message of `.mm/traces/…request.json`. | 39 | - **What the Dungeon Master read**: `AGENT_TRACE=true mm dungeon_master.yaml`, then the last user message of `.mm/traces/…request.json`. |
| 39 | 40 | ||
modified
demos/decision-dnd/README.fr.md +2 -1 | @@ -33,7 +33,8 @@ mm -config spell.yaml -p "An ogre is raising its club to hit me right now." # | ||
| 33 | 33 | |
| 34 | 34 | ## Voir ce que le décideur a répondu |
| 35 | 35 | |
| 36 | -- **Le décideur seul** : `mm -config spell.yaml -p "…"` affiche le JSON du serveur (ajoutez `| jq`). | |
| 36 | +- **Dans le chat, avant chaque réponse** : `spell.yaml` pose `display: true`, si bien que les réponses du décideur apparaissent en JSON indenté, en bleu sous `‣ hook`, entre votre question et la réponse du maître du jeu (un bloc délimité dans un éditeur). Retirez la ligne pour les masquer. | |
| 37 | +- **Le décideur seul** : `mm -config spell.yaml -p "…"` affiche le JSON du serveur (ajoutez `| jq`), précédé de `"display":true`. | |
| 37 | 38 | - **Un fichier JSON par question** : `DECIDER_DUMP=.mm/decider` dans la commande du hook (activé dans `dungeon_master.yaml` ; retirez-le pour arrêter). Chaque question laisse `.mm/decider/<heure>-spell.json` avec la question, la réponse complète du décideur, la ligne transmise au maître du jeu — ou l'erreur quand le décideur a échoué. `.mm/` est ignoré par git. |
| 38 | 39 | - **Ce que le maître du jeu a lu** : `AGENT_TRACE=true mm dungeon_master.yaml`, puis le dernier message utilisateur de `.mm/traces/…request.json`. |
| 39 | 40 | |
| @@ -33,7 +33,8 @@ mm -config spell.yaml -p "An ogre is raising its club to hit me right now." # | |||
| 33 | 33 | ||
| 34 | ## Voir ce que le décideur a répondu | 34 | ## Voir ce que le décideur a répondu |
| 35 | 35 | ||
| 36 | -- **Le décideur seul** : `mm -config spell.yaml -p "…"` affiche le JSON du serveur (ajoutez `| jq`). | 36 | +- **Dans le chat, avant chaque réponse** : `spell.yaml` pose `display: true`, si bien que les réponses du décideur apparaissent en JSON indenté, en bleu sous `‣ hook`, entre votre question et la réponse du maître du jeu (un bloc délimité dans un éditeur). Retirez la ligne pour les masquer. |
| 37 | +- **Le décideur seul** : `mm -config spell.yaml -p "…"` affiche le JSON du serveur (ajoutez `| jq`), précédé de `"display":true`. | ||
| 37 | - **Un fichier JSON par question** : `DECIDER_DUMP=.mm/decider` dans la commande du hook (activé dans `dungeon_master.yaml` ; retirez-le pour arrêter). Chaque question laisse `.mm/decider/<heure>-spell.json` avec la question, la réponse complète du décideur, la ligne transmise au maître du jeu — ou l'erreur quand le décideur a échoué. `.mm/` est ignoré par git. | 38 | - **Un fichier JSON par question** : `DECIDER_DUMP=.mm/decider` dans la commande du hook (activé dans `dungeon_master.yaml` ; retirez-le pour arrêter). Chaque question laisse `.mm/decider/<heure>-spell.json` avec la question, la réponse complète du décideur, la ligne transmise au maître du jeu — ou l'erreur quand le décideur a échoué. `.mm/` est ignoré par git. |
| 38 | - **Ce que le maître du jeu a lu** : `AGENT_TRACE=true mm dungeon_master.yaml`, puis le dernier message utilisateur de `.mm/traces/…request.json`. | 39 | - **Ce que le maître du jeu a lu** : `AGENT_TRACE=true mm dungeon_master.yaml`, puis le dernier message utilisateur de `.mm/traces/…request.json`. |
| 39 | 40 | ||
modified
demos/decision-dnd/hooks/ask-decider/main.go +28 -1 | @@ -13,6 +13,11 @@ | ||
| 13 | 13 | // order, so changing the decider's questions needs no change here. Set |
| 14 | 14 | // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines. |
| 15 | 15 | // |
| 16 | +// When the decider's file sets `decision.display: true`, `mm -p` marks its | |
| 17 | +// JSON with `"display": true`, and the reply also carries `display`: the | |
| 18 | +// answers, indented, which mm shows the user (in its own colour) before the | |
| 19 | +// chat model answers — never to the model. | |
| 20 | +// | |
| 16 | 21 | // The decider file is the first argument, relative to the folder of the agent |
| 17 | 22 | // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml: |
| 18 | 23 | // |
| @@ -87,7 +92,11 @@ func main() { | ||
| 87 | 92 | return |
| 88 | 93 | } |
| 89 | 94 | rec.Context = context |
| 90 | - reply, _ := json.Marshal(map[string]string{"context": context}) | |
| 95 | + fields := map[string]string{"context": context} | |
| 96 | + if display := displayFor(payload); display != "" { | |
| 97 | + fields["display"] = display | |
| 98 | + } | |
| 99 | + reply, _ := json.Marshal(fields) | |
| 91 | 100 | fmt.Println(string(reply)) |
| 92 | 101 | } |
| 93 | 102 | |
| @@ -153,6 +162,24 @@ func contextFor(payload []byte, raw bool) (string, error) { | ||
| 153 | 162 | return strings.Join(lines, "\n"), nil |
| 154 | 163 | } |
| 155 | 164 | |
| 165 | +// displayFor returns the decider's answers, indented, when its file asked | |
| 166 | +// for them to be shown (`"display": true` in the JSON of `mm -p`); "" | |
| 167 | +// otherwise, or when the payload cannot be read. | |
| 168 | +func displayFor(payload []byte) string { | |
| 169 | + var envelope struct { | |
| 170 | + Display bool `json:"display"` | |
| 171 | + Answers json.RawMessage `json:"answers"` | |
| 172 | + } | |
| 173 | + if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 { | |
| 174 | + return "" | |
| 175 | + } | |
| 176 | + var indented bytes.Buffer | |
| 177 | + if json.Indent(&indented, envelope.Answers, "", " ") != nil { | |
| 178 | + return "" | |
| 179 | + } | |
| 180 | + return indented.String() | |
| 181 | +} | |
| 182 | + | |
| 156 | 183 | // inOrder decodes the answers object keeping the order of its keys, which is |
| 157 | 184 | // the order of the decider's questions (a Go map would lose it). |
| 158 | 185 | func inOrder(object json.RawMessage) ([]named, error) { |
| @@ -13,6 +13,11 @@ | |||
| 13 | // order, so changing the decider's questions needs no change here. Set | 13 | // order, so changing the decider's questions needs no change here. Set |
| 14 | // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines. | 14 | // DECIDER_RAW=1 to hand over the server's JSON answers instead of the lines. |
| 15 | // | 15 | // |
| 16 | +// When the decider's file sets `decision.display: true`, `mm -p` marks its | ||
| 17 | +// JSON with `"display": true`, and the reply also carries `display`: the | ||
| 18 | +// answers, indented, which mm shows the user (in its own colour) before the | ||
| 19 | +// chat model answers — never to the model. | ||
| 20 | +// | ||
| 16 | // The decider file is the first argument, relative to the folder of the agent | 21 | // The decider file is the first argument, relative to the folder of the agent |
| 17 | // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml: | 22 | // file that declares the hook (MM_CONFIG_DIR); without one, agent.decider.yaml: |
| 18 | // | 23 | // |
| @@ -87,7 +92,11 @@ func main() { | |||
| 87 | return | 92 | return |
| 88 | } | 93 | } |
| 89 | rec.Context = context | 94 | rec.Context = context |
| 90 | - reply, _ := json.Marshal(map[string]string{"context": context}) | 95 | + fields := map[string]string{"context": context} |
| 96 | + if display := displayFor(payload); display != "" { | ||
| 97 | + fields["display"] = display | ||
| 98 | + } | ||
| 99 | + reply, _ := json.Marshal(fields) | ||
| 91 | fmt.Println(string(reply)) | 100 | fmt.Println(string(reply)) |
| 92 | } | 101 | } |
| 93 | 102 | ||
| @@ -153,6 +162,24 @@ func contextFor(payload []byte, raw bool) (string, error) { | |||
| 153 | return strings.Join(lines, "\n"), nil | 162 | return strings.Join(lines, "\n"), nil |
| 154 | } | 163 | } |
| 155 | 164 | ||
| 165 | +// displayFor returns the decider's answers, indented, when its file asked | ||
| 166 | +// for them to be shown (`"display": true` in the JSON of `mm -p`); "" | ||
| 167 | +// otherwise, or when the payload cannot be read. | ||
| 168 | +func displayFor(payload []byte) string { | ||
| 169 | + var envelope struct { | ||
| 170 | + Display bool `json:"display"` | ||
| 171 | + Answers json.RawMessage `json:"answers"` | ||
| 172 | + } | ||
| 173 | + if json.Unmarshal(payload, &envelope) != nil || !envelope.Display || len(envelope.Answers) == 0 { | ||
| 174 | + return "" | ||
| 175 | + } | ||
| 176 | + var indented bytes.Buffer | ||
| 177 | + if json.Indent(&indented, envelope.Answers, "", " ") != nil { | ||
| 178 | + return "" | ||
| 179 | + } | ||
| 180 | + return indented.String() | ||
| 181 | +} | ||
| 182 | + | ||
| 156 | // inOrder decodes the answers object keeping the order of its keys, which is | 183 | // inOrder decodes the answers object keeping the order of its keys, which is |
| 157 | // the order of the decider's questions (a Go map would lose it). | 184 | // the order of the decider's questions (a Go map would lose it). |
| 158 | func inOrder(object json.RawMessage) ([]named, error) { | 185 | func inOrder(object json.RawMessage) ([]named, error) { |
modified
demos/decision-dnd/hooks/ask-decider/main_test.go +17 -0 | @@ -59,3 +59,20 @@ func TestDeciderFileDefaultsToAgentDeciderYaml(t *testing.T) { | ||
| 59 | 59 | t.Fatalf("one argument: got %q", got) |
| 60 | 60 | } |
| 61 | 61 | } |
| 62 | + | |
| 63 | +func TestDisplayForIndentsTheAnswersWhenTheDeciderAsks(t *testing.T) { | |
| 64 | + marked := `{"display":true,"model":"d1","answers":{"spell":{"type":"choice","choice":"sleep","confidence":0.8}}}` | |
| 65 | + want := "{\n \"spell\": {\n \"type\": \"choice\",\n \"choice\": \"sleep\",\n \"confidence\": 0.8\n }\n}" | |
| 66 | + if got := displayFor([]byte(marked)); got != want { | |
| 67 | + t.Fatalf("got\n%s\nwant\n%s", got, want) | |
| 68 | + } | |
| 69 | +} | |
| 70 | + | |
| 71 | +func TestDisplayForIsEmptyWithoutTheFlag(t *testing.T) { | |
| 72 | + if got := displayFor([]byte(payload)); got != "" { | |
| 73 | + t.Fatalf("got %q, want nothing to show", got) | |
| 74 | + } | |
| 75 | + if got := displayFor([]byte("not json")); got != "" { | |
| 76 | + t.Fatalf("got %q", got) | |
| 77 | + } | |
| 78 | +} | |
| @@ -59,3 +59,20 @@ func TestDeciderFileDefaultsToAgentDeciderYaml(t *testing.T) { | |||
| 59 | t.Fatalf("one argument: got %q", got) | 59 | t.Fatalf("one argument: got %q", got) |
| 60 | } | 60 | } |
| 61 | } | 61 | } |
| 62 | + | ||
| 63 | +func TestDisplayForIndentsTheAnswersWhenTheDeciderAsks(t *testing.T) { | ||
| 64 | + marked := `{"display":true,"model":"d1","answers":{"spell":{"type":"choice","choice":"sleep","confidence":0.8}}}` | ||
| 65 | + want := "{\n \"spell\": {\n \"type\": \"choice\",\n \"choice\": \"sleep\",\n \"confidence\": 0.8\n }\n}" | ||
| 66 | + if got := displayFor([]byte(marked)); got != want { | ||
| 67 | + t.Fatalf("got\n%s\nwant\n%s", got, want) | ||
| 68 | + } | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +func TestDisplayForIsEmptyWithoutTheFlag(t *testing.T) { | ||
| 72 | + if got := displayFor([]byte(payload)); got != "" { | ||
| 73 | + t.Fatalf("got %q, want nothing to show", got) | ||
| 74 | + } | ||
| 75 | + if got := displayFor([]byte("not json")); got != "" { | ||
| 76 | + t.Fatalf("got %q", got) | ||
| 77 | + } | ||
| 78 | +} | ||
modified
demos/decision-dnd/spell.yaml +3 -0 | @@ -5,6 +5,9 @@ baseUrl: http://127.0.0.1:17434/v1 | ||
| 5 | 5 | fallback: http://host.docker.internal:17434/v1 |
| 6 | 6 | |
| 7 | 7 | decision: |
| 8 | + # The chat shows this decider's answers (indented JSON, in its own colour) | |
| 9 | + # before the dungeon master answers: -p marks its JSON, the hook passes it on. | |
| 10 | + display: true | |
| 8 | 11 | questions: |
| 9 | 12 | spell: |
| 10 | 13 | type: choice |
| @@ -5,6 +5,9 @@ baseUrl: http://127.0.0.1:17434/v1 | |||
| 5 | fallback: http://host.docker.internal:17434/v1 | 5 | fallback: http://host.docker.internal:17434/v1 |
| 6 | 6 | ||
| 7 | decision: | 7 | decision: |
| 8 | + # The chat shows this decider's answers (indented JSON, in its own colour) | ||
| 9 | + # before the dungeon master answers: -p marks its JSON, the hook passes it on. | ||
| 10 | + display: true | ||
| 8 | questions: | 11 | questions: |
| 9 | spell: | 12 | spell: |
| 10 | type: choice | 13 | type: choice |
added
dev.team/workspace/README.md +156 -0 | new file mode 100644 | ||
| @@ -0,0 +1,156 @@ | ||
| 1 | +# Issue Tracker | |
| 2 | + | |
| 3 | +A simple issue tracking application built with Go and the Gin web framework. | |
| 4 | + | |
| 5 | +## Description | |
| 6 | + | |
| 7 | +Issue Tracker is a lightweight web application for managing project issues. It provides a clean, browser-based interface for creating, viewing, editing, and deleting issues, with support for status and priority management. Data is persisted in a local SQLite database, making it easy to run without any external dependencies. | |
| 8 | + | |
| 9 | +## Features | |
| 10 | + | |
| 11 | +- **Full CRUD operations** — create, read, update, and delete issues | |
| 12 | +- **Status management** — track issues through their lifecycle: `open`, `in_progress`, `done`, `closed` | |
| 13 | +- **Priority levels** — assign priorities: `low`, `medium`, `high`, `critical` | |
| 14 | +- **Assignee tracking** — assign issues to team members | |
| 15 | +- **Issue detail view** — view full details of a single issue | |
| 16 | +- **Form validation** — title is required; validation errors are displayed inline | |
| 17 | +- **Delete confirmation** — browser confirmation dialog before deleting an issue | |
| 18 | +- **Externalized templates** — HTML templates and CSS live in a dedicated `templates/` directory, making styling and layout changes straightforward | |
| 19 | +- **Responsive styling** — a custom stylesheet (`style.css`) with badges, buttons, and form styling for a polished look | |
| 20 | + | |
| 21 | +## Technology Stack | |
| 22 | + | |
| 23 | +| Component | Technology | | |
| 24 | +|-----------|------------| | |
| 25 | +| Language | [Go](https://go.dev/) 1.27 | | |
| 26 | +| Web framework | [Gin](https://gin-gonic.com/) v1.10.0 | | |
| 27 | +| Database | [SQLite](https://www.sqlite.org/) via [go-sqlite3](https://github.com/mattn/go-sqlite3) | | |
| 28 | +| Templates | Go `html/template` (server-side rendered) | | |
| 29 | + | |
| 30 | +## Installation / Setup | |
| 31 | + | |
| 32 | +### Prerequisites | |
| 33 | + | |
| 34 | +- [Go](https://go.dev/dl/) 1.27 or later | |
| 35 | +- A C compiler (required by `go-sqlite3` for cgo) | |
| 36 | + | |
| 37 | +### Build and Run | |
| 38 | + | |
| 39 | +```bash | |
| 40 | +# Clone the repository | |
| 41 | +git clone <repository-url> | |
| 42 | +cd issueapp | |
| 43 | + | |
| 44 | +# Download dependencies | |
| 45 | +go mod download | |
| 46 | + | |
| 47 | +# Run the application (default port 8080) | |
| 48 | +go run ./cmd/issueapp | |
| 49 | + | |
| 50 | +# Or build a binary first | |
| 51 | +go build -o issueapp ./cmd/issueapp | |
| 52 | +./issueapp | |
| 53 | +``` | |
| 54 | + | |
| 55 | +The application listens on `http://localhost:8080` by default. A SQLite database file (`issues.db`) is created automatically in the working directory on first run. | |
| 56 | + | |
| 57 | +### Configuration | |
| 58 | + | |
| 59 | +| Flag | Default | Description | | |
| 60 | +|------|---------|-------------| | |
| 61 | +| `-port` | `8080` | HTTP port to listen on | | |
| 62 | + | |
| 63 | +```bash | |
| 64 | +# Run on a custom port | |
| 65 | +go run ./cmd/issueapp -port 3000 | |
| 66 | +``` | |
| 67 | + | |
| 68 | +## Usage | |
| 69 | + | |
| 70 | +The application is a server-rendered web app. Open your browser and navigate to the base URL. | |
| 71 | + | |
| 72 | +### Routes | |
| 73 | + | |
| 74 | +| Method | Path | Description | | |
| 75 | +|--------|------|-------------| | |
| 76 | +| `GET` | `/` | Redirects to `/issues` | | |
| 77 | +| `GET` | `/issues` | List all issues | | |
| 78 | +| `GET` | `/issues/new` | Show the "new issue" form | | |
| 79 | +| `POST` | `/issues` | Create a new issue | | |
| 80 | +| `GET` | `/issues/:id` | Show a single issue's detail page | | |
| 81 | +| `GET` | `/issues/:id/edit` | Show the edit form for an issue | | |
| 82 | +| `POST` | `/issues/:id` | Update an existing issue | | |
| 83 | +| `POST` | `/issues/:id/delete` | Delete an issue | | |
| 84 | + | |
| 85 | +### Issue Fields | |
| 86 | + | |
| 87 | +| Field | Type | Notes | | |
| 88 | +|-------|------|-------| | |
| 89 | +| `title` | string | **Required** | | |
| 90 | +| `description` | string | Free-form text | | |
| 91 | +| `status` | string | One of: `open`, `in_progress`, `done`, `closed` | | |
| 92 | +| `priority` | string | One of: `low`, `medium`, `high`, `critical` | | |
| 93 | +| `assignee` | string | Free-form name | | |
| 94 | + | |
| 95 | +### Example Workflow | |
| 96 | + | |
| 97 | +1. Navigate to `http://localhost:8080/issues` to see the issue list. | |
| 98 | +2. Click **+ New Issue** (or go to `/issues/new`) to create an issue. | |
| 99 | +3. Fill in the title, description, status, priority, and assignee, then submit. | |
| 100 | +4. You are redirected to the issue detail page (`/issues/:id`). | |
| 101 | +5. From the detail page you can **Edit** or **Delete** the issue. | |
| 102 | + | |
| 103 | +## Project Structure | |
| 104 | + | |
| 105 | +``` | |
| 106 | +. | |
| 107 | +├── cmd/ | |
| 108 | +│ └── issueapp/ | |
| 109 | +│ └── main.go # Entry point: flags, DB init, router setup | |
| 110 | +├── internal/ | |
| 111 | +│ └── app/ | |
| 112 | +│ ├── db.go # SQLite connection, schema, CRUD queries | |
| 113 | +│ ├── handlers.go # HTTP handlers and route registration | |
| 114 | +│ ├── models.go # Issue struct and valid status/priority values | |
| 115 | +│ ├── templates.go # Template and CSS loading from templates/ dir | |
| 116 | +│ └── review_test.go # HTTP-level tests for the app package | |
| 117 | +├── templates/ | |
| 118 | +│ ├── layout.html # Shared HTML layout (header, nav, CSS include) | |
| 119 | +│ ├── list.html # Issue list and issue detail views | |
| 120 | +│ ├── form.html # Create/edit issue form | |
| 121 | +│ └── style.css # Application stylesheet | |
| 122 | +├── review_test.go # HTTP-level tests (root package) | |
| 123 | +├── go.mod | |
| 124 | +├── go.sum | |
| 125 | +└── issues.db # SQLite database (created at runtime) | |
| 126 | +``` | |
| 127 | + | |
| 128 | +## Recent Changes | |
| 129 | + | |
| 130 | +- **Template externalization** — HTML templates and CSS were moved out of Go source files into the `templates/` directory. Templates are now loaded at startup via `loadTemplates()`, which reads `style.css`, `layout.html`, `list.html`, and `form.html` from disk. This makes it easy to adjust the UI without recompiling Go code. | |
| 131 | +- **CSS improvements** — a dedicated `style.css` stylesheet was introduced with styled badges for status and priority, consistent button styles, form styling, and a clean layout. The CSS is injected into the shared `layout.html` template. | |
| 132 | + | |
| 133 | +## Testing | |
| 134 | + | |
| 135 | +The project includes HTTP-level integration tests that exercise the router and handlers against a temporary in-memory database. | |
| 136 | + | |
| 137 | +```bash | |
| 138 | +# Run all tests | |
| 139 | +go test ./... | |
| 140 | + | |
| 141 | +# Run with verbose output | |
| 142 | +go test -v ./... | |
| 143 | + | |
| 144 | +# Run a specific test | |
| 145 | +go test -v -run TestTemplates ./internal/app/ | |
| 146 | +``` | |
| 147 | + | |
| 148 | +The tests cover: | |
| 149 | + | |
| 150 | +- Listing issues (`GET /issues`) | |
| 151 | +- Showing the create form (`GET /issues/new`) | |
| 152 | +- Handling a non-existent issue (`GET /issues/99999` → 404) | |
| 153 | + | |
| 154 | +## License | |
| 155 | + | |
| 156 | +This project is licensed under the [MIT License](LICENSE). | |
| new file mode 100644 | |||
| @@ -0,0 +1,156 @@ | |||
| 1 | +# Issue Tracker | ||
| 2 | + | ||
| 3 | +A simple issue tracking application built with Go and the Gin web framework. | ||
| 4 | + | ||
| 5 | +## Description | ||
| 6 | + | ||
| 7 | +Issue Tracker is a lightweight web application for managing project issues. It provides a clean, browser-based interface for creating, viewing, editing, and deleting issues, with support for status and priority management. Data is persisted in a local SQLite database, making it easy to run without any external dependencies. | ||
| 8 | + | ||
| 9 | +## Features | ||
| 10 | + | ||
| 11 | +- **Full CRUD operations** — create, read, update, and delete issues | ||
| 12 | +- **Status management** — track issues through their lifecycle: `open`, `in_progress`, `done`, `closed` | ||
| 13 | +- **Priority levels** — assign priorities: `low`, `medium`, `high`, `critical` | ||
| 14 | +- **Assignee tracking** — assign issues to team members | ||
| 15 | +- **Issue detail view** — view full details of a single issue | ||
| 16 | +- **Form validation** — title is required; validation errors are displayed inline | ||
| 17 | +- **Delete confirmation** — browser confirmation dialog before deleting an issue | ||
| 18 | +- **Externalized templates** — HTML templates and CSS live in a dedicated `templates/` directory, making styling and layout changes straightforward | ||
| 19 | +- **Responsive styling** — a custom stylesheet (`style.css`) with badges, buttons, and form styling for a polished look | ||
| 20 | + | ||
| 21 | +## Technology Stack | ||
| 22 | + | ||
| 23 | +| Component | Technology | | ||
| 24 | +|-----------|------------| | ||
| 25 | +| Language | [Go](https://go.dev/) 1.27 | | ||
| 26 | +| Web framework | [Gin](https://gin-gonic.com/) v1.10.0 | | ||
| 27 | +| Database | [SQLite](https://www.sqlite.org/) via [go-sqlite3](https://github.com/mattn/go-sqlite3) | | ||
| 28 | +| Templates | Go `html/template` (server-side rendered) | | ||
| 29 | + | ||
| 30 | +## Installation / Setup | ||
| 31 | + | ||
| 32 | +### Prerequisites | ||
| 33 | + | ||
| 34 | +- [Go](https://go.dev/dl/) 1.27 or later | ||
| 35 | +- A C compiler (required by `go-sqlite3` for cgo) | ||
| 36 | + | ||
| 37 | +### Build and Run | ||
| 38 | + | ||
| 39 | +```bash | ||
| 40 | +# Clone the repository | ||
| 41 | +git clone <repository-url> | ||
| 42 | +cd issueapp | ||
| 43 | + | ||
| 44 | +# Download dependencies | ||
| 45 | +go mod download | ||
| 46 | + | ||
| 47 | +# Run the application (default port 8080) | ||
| 48 | +go run ./cmd/issueapp | ||
| 49 | + | ||
| 50 | +# Or build a binary first | ||
| 51 | +go build -o issueapp ./cmd/issueapp | ||
| 52 | +./issueapp | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +The application listens on `http://localhost:8080` by default. A SQLite database file (`issues.db`) is created automatically in the working directory on first run. | ||
| 56 | + | ||
| 57 | +### Configuration | ||
| 58 | + | ||
| 59 | +| Flag | Default | Description | | ||
| 60 | +|------|---------|-------------| | ||
| 61 | +| `-port` | `8080` | HTTP port to listen on | | ||
| 62 | + | ||
| 63 | +```bash | ||
| 64 | +# Run on a custom port | ||
| 65 | +go run ./cmd/issueapp -port 3000 | ||
| 66 | +``` | ||
| 67 | + | ||
| 68 | +## Usage | ||
| 69 | + | ||
| 70 | +The application is a server-rendered web app. Open your browser and navigate to the base URL. | ||
| 71 | + | ||
| 72 | +### Routes | ||
| 73 | + | ||
| 74 | +| Method | Path | Description | | ||
| 75 | +|--------|------|-------------| | ||
| 76 | +| `GET` | `/` | Redirects to `/issues` | | ||
| 77 | +| `GET` | `/issues` | List all issues | | ||
| 78 | +| `GET` | `/issues/new` | Show the "new issue" form | | ||
| 79 | +| `POST` | `/issues` | Create a new issue | | ||
| 80 | +| `GET` | `/issues/:id` | Show a single issue's detail page | | ||
| 81 | +| `GET` | `/issues/:id/edit` | Show the edit form for an issue | | ||
| 82 | +| `POST` | `/issues/:id` | Update an existing issue | | ||
| 83 | +| `POST` | `/issues/:id/delete` | Delete an issue | | ||
| 84 | + | ||
| 85 | +### Issue Fields | ||
| 86 | + | ||
| 87 | +| Field | Type | Notes | | ||
| 88 | +|-------|------|-------| | ||
| 89 | +| `title` | string | **Required** | | ||
| 90 | +| `description` | string | Free-form text | | ||
| 91 | +| `status` | string | One of: `open`, `in_progress`, `done`, `closed` | | ||
| 92 | +| `priority` | string | One of: `low`, `medium`, `high`, `critical` | | ||
| 93 | +| `assignee` | string | Free-form name | | ||
| 94 | + | ||
| 95 | +### Example Workflow | ||
| 96 | + | ||
| 97 | +1. Navigate to `http://localhost:8080/issues` to see the issue list. | ||
| 98 | +2. Click **+ New Issue** (or go to `/issues/new`) to create an issue. | ||
| 99 | +3. Fill in the title, description, status, priority, and assignee, then submit. | ||
| 100 | +4. You are redirected to the issue detail page (`/issues/:id`). | ||
| 101 | +5. From the detail page you can **Edit** or **Delete** the issue. | ||
| 102 | + | ||
| 103 | +## Project Structure | ||
| 104 | + | ||
| 105 | +``` | ||
| 106 | +. | ||
| 107 | +├── cmd/ | ||
| 108 | +│ └── issueapp/ | ||
| 109 | +│ └── main.go # Entry point: flags, DB init, router setup | ||
| 110 | +├── internal/ | ||
| 111 | +│ └── app/ | ||
| 112 | +│ ├── db.go # SQLite connection, schema, CRUD queries | ||
| 113 | +│ ├── handlers.go # HTTP handlers and route registration | ||
| 114 | +│ ├── models.go # Issue struct and valid status/priority values | ||
| 115 | +│ ├── templates.go # Template and CSS loading from templates/ dir | ||
| 116 | +│ └── review_test.go # HTTP-level tests for the app package | ||
| 117 | +├── templates/ | ||
| 118 | +│ ├── layout.html # Shared HTML layout (header, nav, CSS include) | ||
| 119 | +│ ├── list.html # Issue list and issue detail views | ||
| 120 | +│ ├── form.html # Create/edit issue form | ||
| 121 | +│ └── style.css # Application stylesheet | ||
| 122 | +├── review_test.go # HTTP-level tests (root package) | ||
| 123 | +├── go.mod | ||
| 124 | +├── go.sum | ||
| 125 | +└── issues.db # SQLite database (created at runtime) | ||
| 126 | +``` | ||
| 127 | + | ||
| 128 | +## Recent Changes | ||
| 129 | + | ||
| 130 | +- **Template externalization** — HTML templates and CSS were moved out of Go source files into the `templates/` directory. Templates are now loaded at startup via `loadTemplates()`, which reads `style.css`, `layout.html`, `list.html`, and `form.html` from disk. This makes it easy to adjust the UI without recompiling Go code. | ||
| 131 | +- **CSS improvements** — a dedicated `style.css` stylesheet was introduced with styled badges for status and priority, consistent button styles, form styling, and a clean layout. The CSS is injected into the shared `layout.html` template. | ||
| 132 | + | ||
| 133 | +## Testing | ||
| 134 | + | ||
| 135 | +The project includes HTTP-level integration tests that exercise the router and handlers against a temporary in-memory database. | ||
| 136 | + | ||
| 137 | +```bash | ||
| 138 | +# Run all tests | ||
| 139 | +go test ./... | ||
| 140 | + | ||
| 141 | +# Run with verbose output | ||
| 142 | +go test -v ./... | ||
| 143 | + | ||
| 144 | +# Run a specific test | ||
| 145 | +go test -v -run TestTemplates ./internal/app/ | ||
| 146 | +``` | ||
| 147 | + | ||
| 148 | +The tests cover: | ||
| 149 | + | ||
| 150 | +- Listing issues (`GET /issues`) | ||
| 151 | +- Showing the create form (`GET /issues/new`) | ||
| 152 | +- Handling a non-existent issue (`GET /issues/99999` → 404) | ||
| 153 | + | ||
| 154 | +## License | ||
| 155 | + | ||
| 156 | +This project is licensed under the [MIT License](LICENSE). | ||
modified
dev.team/workspace/issues.db +0 -0 | Binary files a/dev.team/workspace/issues.db and b/dev.team/workspace/issues.db differ |
| Binary files a/dev.team/workspace/issues.db and b/dev.team/workspace/issues.db differ | Binary files a/dev.team/workspace/issues.db and b/dev.team/workspace/issues.db differ |
modified
docs/diagrams/packages.drawio +2 -1 | @@ -1,3 +1,4 @@ | ||
| 1 | 1 | <mxfile host="app.diagrams.net" modified="2026-10-22T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1060"><root><mxCell id="0" /><mxCell id="1" parent="0" /><mxCell id="main" value="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry" /></mxCell><mxCell id="agent" value="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="acp" value="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="session" value="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="compact" value="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="engine" value="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="tools" value="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content; declared toolsbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content</font>lt;/fontbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content</font>gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="detector" value="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="config" value="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="spinner" value="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="ui" value="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="fileedit" value="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="skills" value="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="genkit" value="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="acpsdk" value="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="openai" value="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="yaml" value="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="version" value="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry" /></mxCell> |
| 2 | 2 | <mxCell id="e61" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="version"><mxGeometry relative="1" as="geometry" /></mxCell> |
| 3 | - <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="mention" value="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="store" value="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json), the questions ↑ recalls, /memory and search_memory</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry" /></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="tui" value="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry" /></mxCell><mxCell id="command" value="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="tcell" value="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="saved" value="<b>internal/saved</b><br/><font style="font-size:10px">an answer to a new file: /save, save_answer</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e87" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e88" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e89" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="lsp" value="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry" /></mxCell><mxCell id="lspdev" value="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry" /></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="legend" value="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry" /></mxCell><mxCell id="webmd" value="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry" /></mxCell><mxCell id="xnet" value="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry" /></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ask" value="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="consent" value="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook, a hook's one-call approval</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry" /></mxCell><mxCell id="mcp" value="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70consent" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="structured" value="<b>internal/structured</b><br/><font style="font-size:10px">structuredOutput: Go declarations → JSON Schema (go/parser)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e75" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="structured"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentsmd" value="<b>internal/agentsmd</b><br/><font style="font-size:10px">the project's AGENTS.md, appended to the system prompt per conversation</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="520.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e76" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e77" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e78" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e79" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e80" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentsmd" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="elicit" value="<b>internal/elicit</b><br/><font style="font-size:10px">ask_user's questions and answers: normalize, line answers, elicitation form schema</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="780.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e81" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e82" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e83" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e84" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e85" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e86" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="hooks" value="<b>internal/hooks</b><br/><font style="font-size:10px">the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="940" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_engine_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_main_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_session" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_store" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="docsearch" value="<b>internal/docsearch</b><br/><font style="font-size:10px">BM25 over the text files of folders: /docs, search_docs</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="textfold" value="<b>internal/textfold</b><br/><font style="font-size:10px">case and accents folded, rune for rune</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_docsearch_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_docsearch_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_textfold_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="textfold" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentinit" value="<b>internal/agentinit</b><br/><font style="font-size:10px">mm init: the agent.yaml wizard, presets.yaml, the server's model facts</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="660" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_elicit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="wasmtool" value="<b>internal/wasmtool</b><br/><font style="font-size:10px">runs the wasm tools and hooks: JSON on stdin, declared folders only, timeout</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="360" width="200" height="60" as="geometry" /></mxCell><mxCell id="wazero" value="<b>github.com/tetratelabs/wazero</b><br/><font style="font-size:10px">WebAssembly runtime in pure Go, WASI preview 1</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1640" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_tools_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_wasmtool_wazero" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="wasmtool" target="wazero"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="decide" value="<b>internal/decide</b><br/><font style="font-size:10px">decision models: /v1/systemone client, skill and tool routing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1140" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="ed0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="decide" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell></root></mxGraphModel></diagram></mxfile> | |
| \ No newline at end of file | ||
| 3 | + <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="mention" value="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="store" value="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json), the questions ↑ recalls, /memory and search_memory</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry" /></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="tui" value="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry" /></mxCell><mxCell id="command" value="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="tcell" value="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="saved" value="<b>internal/saved</b><br/><font style="font-size:10px">an answer to a new file: /save, save_answer</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e87" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e88" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e89" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="lsp" value="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry" /></mxCell><mxCell id="lspdev" value="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry" /></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="legend" value="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry" /></mxCell><mxCell id="webmd" value="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry" /></mxCell><mxCell id="xnet" value="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry" /></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ask" value="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="consent" value="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook, a hook's one-call approval</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry" /></mxCell><mxCell id="mcp" value="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70consent" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="structured" value="<b>internal/structured</b><br/><font style="font-size:10px">structuredOutput: Go declarations → JSON Schema (go/parser)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e75" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="structured"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentsmd" value="<b>internal/agentsmd</b><br/><font style="font-size:10px">the project's AGENTS.md, appended to the system prompt per conversation</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="520.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e76" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e77" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e78" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e79" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e80" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentsmd" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="elicit" value="<b>internal/elicit</b><br/><font style="font-size:10px">ask_user's questions and answers: normalize, line answers, elicitation form schema</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="780.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e81" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell> | |
| 4 | + <mxCell id="e90" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e82" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e83" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e84" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e85" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e86" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="hooks" value="<b>internal/hooks</b><br/><font style="font-size:10px">the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="940" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_engine_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_main_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_session" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_store" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="docsearch" value="<b>internal/docsearch</b><br/><font style="font-size:10px">BM25 over the text files of folders: /docs, search_docs</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="textfold" value="<b>internal/textfold</b><br/><font style="font-size:10px">case and accents folded, rune for rune</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_docsearch_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_docsearch_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_textfold_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="textfold" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentinit" value="<b>internal/agentinit</b><br/><font style="font-size:10px">mm init: the agent.yaml wizard, presets.yaml, the server's model facts</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="660" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_elicit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="wasmtool" value="<b>internal/wasmtool</b><br/><font style="font-size:10px">runs the wasm tools and hooks: JSON on stdin, declared folders only, timeout</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="360" width="200" height="60" as="geometry" /></mxCell><mxCell id="wazero" value="<b>github.com/tetratelabs/wazero</b><br/><font style="font-size:10px">WebAssembly runtime in pure Go, WASI preview 1</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1640" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_tools_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_wasmtool_wazero" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="wasmtool" target="wazero"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="decide" value="<b>internal/decide</b><br/><font style="font-size:10px">decision models: /v1/systemone client, skill and tool routing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1140" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="ed0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="decide" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell></root></mxGraphModel></diagram></mxfile> | |
| \ No newline at end of file | ||
| @@ -1,3 +1,4 @@ | |||
| 1 | <mxfile host="app.diagrams.net" modified="2026-10-22T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1060"><root><mxCell id="0" /><mxCell id="1" parent="0" /><mxCell id="main" value="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry" /></mxCell><mxCell id="agent" value="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="acp" value="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="session" value="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="compact" value="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="engine" value="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="tools" value="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content; declared toolsbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content</font>lt;/fontbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content</font>gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="detector" value="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="config" value="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="spinner" value="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="ui" value="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="fileedit" value="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="skills" value="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="genkit" value="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="acpsdk" value="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="openai" value="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="yaml" value="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="version" value="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry" /></mxCell> | 1 | <mxfile host="app.diagrams.net" modified="2026-10-22T00:00:00.000Z" agent="mm docs" version="24.0.0" type="device"><diagram id="packages" name="packages"><mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="1060"><root><mxCell id="0" /><mxCell id="1" parent="0" /><mxCell id="main" value="<b>main (mm)</b><br/><font style="font-size:10px">wiring: config, engine, tools, then one front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="420" y="40" width="200" height="60" as="geometry" /></mxCell><mxCell id="agent" value="<b>internal/agent</b><br/><font style="font-size:10px">terminal REPL: /quit /abort /compact /new</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="160" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="acp" value="<b>internal/acp</b><br/><font style="font-size:10px">editor front end: Agent Client Protocol on stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="700" y="180" width="200" height="60" as="geometry" /></mxCell><mxCell id="session" value="<b>internal/session</b><br/><font style="font-size:10px">what a conversation starts from; the /new reset</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="compact" value="<b>internal/compact</b><br/><font style="font-size:10px">history compression into a model-written summary</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="engine" value="<b>internal/engine</b><br/><font style="font-size:10px">LLM connection + generation, one Provider per server</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="700" y="320" width="200" height="60" as="geometry" /></mxCell><mxCell id="tools" value="<b>internal/tools</b><br/><font style="font-size:10px">built-in tools: bash, read_skill, file tools, lsp, fetch_content; declared toolsbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content</font>lt;/fontbuilt-in tools: bash, read_skill, file tools, lsp, fetch_content</font>gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="detector" value="<b>internal/detector</b><br/><font style="font-size:10px">repeated-action (loop) detection</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="40" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="config" value="<b>internal/config</b><br/><font style="font-size:10px">settings with YAML overrides (agent.yaml)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="spinner" value="<b>internal/spinner</b><br/><font style="font-size:10px">terminal spinner and styling</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="480" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="ui" value="<b>internal/ui</b><br/><font style="font-size:10px">human output destination + shared event sink</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="fileedit" value="<b>internal/fileedit</b><br/><font style="font-size:10px">exact-replacement file editing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="skills" value="<b>internal/skills</b><br/><font style="font-size:10px">markdown skills catalogue (./skills)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="600" width="200" height="60" as="geometry" /></mxCell><mxCell id="genkit" value="<b>github.com/firebase/genkit</b><br/><font style="font-size:10px">ai.Message, tools, model calls</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="160" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="acpsdk" value="<b>github.com/coder/acp-go-sdk</b><br/><font style="font-size:10px">JSON-RPC transport for ACP</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="700" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="openai" value="<b>github.com/openai/openai-go</b><br/><font style="font-size:10px">OpenAI-compatible HTTP client</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="yaml" value="<b>gopkg.in/yaml.v3</b><br/><font style="font-size:10px">YAML parsing</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="400" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="version" value="<b>internal/version</b><br/><font style="font-size:10px">which mm this is: build stamp, else the commit</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1380" y="600" width="200" height="60" as="geometry" /></mxCell> |
| 2 | <mxCell id="e61" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="version"><mxGeometry relative="1" as="geometry" /></mxCell> | 2 | <mxCell id="e61" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="version"><mxGeometry relative="1" as="geometry" /></mxCell> |
| 3 | - <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="mention" value="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="store" value="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json), the questions ↑ recalls, /memory and search_memory</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry" /></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="tui" value="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry" /></mxCell><mxCell id="command" value="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="tcell" value="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="saved" value="<b>internal/saved</b><br/><font style="font-size:10px">an answer to a new file: /save, save_answer</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e87" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e88" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e89" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="lsp" value="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry" /></mxCell><mxCell id="lspdev" value="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry" /></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="legend" value="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry" /></mxCell><mxCell id="webmd" value="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry" /></mxCell><mxCell id="xnet" value="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry" /></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ask" value="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="consent" value="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook, a hook's one-call approval</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry" /></mxCell><mxCell id="mcp" value="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70consent" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="structured" value="<b>internal/structured</b><br/><font style="font-size:10px">structuredOutput: Go declarations → JSON Schema (go/parser)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e75" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="structured"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentsmd" value="<b>internal/agentsmd</b><br/><font style="font-size:10px">the project's AGENTS.md, appended to the system prompt per conversation</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="520.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e76" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e77" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e78" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e79" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e80" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentsmd" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="elicit" value="<b>internal/elicit</b><br/><font style="font-size:10px">ask_user's questions and answers: normalize, line answers, elicitation form schema</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="780.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e81" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e82" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e83" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e84" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e85" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e86" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="hooks" value="<b>internal/hooks</b><br/><font style="font-size:10px">the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="940" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_engine_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_main_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_session" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_store" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="docsearch" value="<b>internal/docsearch</b><br/><font style="font-size:10px">BM25 over the text files of folders: /docs, search_docs</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="textfold" value="<b>internal/textfold</b><br/><font style="font-size:10px">case and accents folded, rune for rune</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_docsearch_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_docsearch_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_textfold_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="textfold" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentinit" value="<b>internal/agentinit</b><br/><font style="font-size:10px">mm init: the agent.yaml wizard, presets.yaml, the server's model facts</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="660" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_elicit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="wasmtool" value="<b>internal/wasmtool</b><br/><font style="font-size:10px">runs the wasm tools and hooks: JSON on stdin, declared folders only, timeout</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="360" width="200" height="60" as="geometry" /></mxCell><mxCell id="wazero" value="<b>github.com/tetratelabs/wazero</b><br/><font style="font-size:10px">WebAssembly runtime in pure Go, WASI preview 1</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1640" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_tools_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_wasmtool_wazero" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="wasmtool" target="wazero"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="decide" value="<b>internal/decide</b><br/><font style="font-size:10px">decision models: /v1/systemone client, skill and tool routing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1140" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="ed0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="decide" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell></root></mxGraphModel></diagram></mxfile> | ||
| \ No newline at end of file | \ No newline at end of file | ||
| 3 | + <mxCell id="e1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="acp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e7" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e8" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e9" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e10" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e11" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e12" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e13" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="acp" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e14" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e15" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e16" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e17" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e18" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e19" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e20" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="agent" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e21" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="compact" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e22" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="compact" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e23" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="config" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e24" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e25" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e26" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e27" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e28" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="engine" target="openai"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e29" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e30" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e31" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="fileedit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e32" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e33" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e34" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e35" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="tools" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e36" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="session" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="mention" value="<b>internal/mention</b><br/><font style="font-size:10px">the @path notation of a question</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e37" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e38" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="store" value="<b>internal/store</b><br/><font style="font-size:10px">per-project session files (.mm/sessions/<id>.json), the questions ↑ recalls, /memory and search_memory</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="480" width="200" height="60" as="geometry" /></mxCell><mxCell id="e39" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e40" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="tui" value="<b>internal/tui</b><br/><font style="font-size:10px">window front end: one screen on tcell (mm -tui)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="430" y="160" width="200" height="60" as="geometry" /></mxCell><mxCell id="command" value="<b>internal/command</b><br/><font style="font-size:10px">the one slash-command table, per front end</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="tcell" value="<b>github.com/gdamore/tcell/v2</b><br/><font style="font-size:10px">terminal screen, keys, mouse</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="920" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="e41" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="tui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e42" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e43" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e44" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e45" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e46" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="mention"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e47" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e48" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e49" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e50" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e51" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tcell"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e52" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e53" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e54" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="command" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="saved" value="<b>internal/saved</b><br/><font style="font-size:10px">an answer to a new file: /save, save_answer</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="920" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e87" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e88" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e89" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="saved"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="clipboard" value="<b>internal/clipboard</b><br/><font style="font-size:10px">system clipboard: native tool, then OSC 52</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1140" y="680" width="200" height="60" as="geometry" /></mxCell><mxCell id="e55" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e56" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="clipboard"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="lsp" value="<b>internal/lsp</b><br/><font style="font-size:10px">language-server client: definition, references, hover, symbols, diagnostics, rename</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1160" y="600" width="220" height="60" as="geometry" /></mxCell><mxCell id="lspdev" value="<b>go.lsp.dev/protocol + jsonrpc2</b><br/><font style="font-size:10px">LSP types and JSON-RPC 2.0 over stdio</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="1160" y="700" width="220" height="60" as="geometry" /></mxCell><mxCell id="e57" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e58" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="lsp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e59" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="lsp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e60" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="lsp" target="lspdev"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="legend" value="<b>mm — package dependencies</b><br/>Arrow: "imports". Yellow: entry point. Blue: front ends. Green: shared logic. Grey: leaf packages. Dashed purple: third-party modules.<br/>Regenerate the edge list from: go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./..." style="text;html=1;whiteSpace=wrap;align=left;verticalAlign=top;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1"><mxGeometry x="900" y="40" width="440" height="90" as="geometry" /></mxCell><mxCell id="webmd" value="<b>internal/webmd</b><br/><font style="font-size:10px">web page → Markdown: fetch (robots.txt, charset → UTF-8), clean, convert</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="460" width="220" height="60" as="geometry" /></mxCell><mxCell id="xnet" value="<b>golang.org/x/net/html (+ x/text)</b><br/><font style="font-size:10px">HTML5 parser, charset lookup</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1400" y="760" width="220" height="60" as="geometry" /></mxCell><mxCell id="e_main_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_webmd" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="webmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_webmd_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="webmd" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ask" value="<b>internal/ask</b><br/><font style="font-size:10px">/ask another mm agent: child mm -acp, ACP client side</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="430" y="640" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_ask" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="ask"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_command" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="command"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_ui" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_acpsdk" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="acpsdk"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_genkit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="genkit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_yaml" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="ask" target="yaml"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="consent" value="<b>internal/consent</b><br/><font style="font-size:10px">yolo switch, the four answers, per-session memory, the line mode prompt hook, a hook's one-call approval</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="700" y="500" width="200" height="60" as="geometry" /></mxCell><mxCell id="mcp" value="<b>internal/mcp</b><br/><font style="font-size:10px">MCP clients (stdio, HTTP), their tools wrapped as mm tools</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1400" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e62" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e63" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="consent" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e64" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e65" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e66" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e67" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e68" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70consent" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e69" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="consent"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e70" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e71" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="ui"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e72" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="mcp" target="detector"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e73" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e74" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="mcp"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="structured" value="<b>internal/structured</b><br/><font style="font-size:10px">structuredOutput: Go declarations → JSON Schema (go/parser)</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="260" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e75" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="structured"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentsmd" value="<b>internal/agentsmd</b><br/><font style="font-size:10px">the project's AGENTS.md, appended to the system prompt per conversation</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="520.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e76" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e77" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e78" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e79" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentsmd"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e80" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentsmd" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="elicit" value="<b>internal/elicit</b><br/><font style="font-size:10px">ask_user's questions and answers: normalize, line answers, elicitation form schema</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="780.0" y="940.0" width="200" height="60" as="geometry" /></mxCell><mxCell id="e81" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell> | ||
| 4 | + <mxCell id="e90" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ui" target="spinner"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e82" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e83" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e84" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e85" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e86" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="hooks" value="<b>internal/hooks</b><br/><font style="font-size:10px">the hooks: block — runs each hook (sh -c, JSON on stdin/stdout), chain, onError</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="940" y="400" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_engine_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_ask_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="ask" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_main_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_session" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="session"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_compact" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="compact"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_hooks" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="hooks"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_store" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="store"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="docsearch" value="<b>internal/docsearch</b><br/><font style="font-size:10px">BM25 over the text files of folders: /docs, search_docs</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="460" width="200" height="60" as="geometry" /></mxCell><mxCell id="textfold" value="<b>internal/textfold</b><br/><font style="font-size:10px">case and accents folded, rune for rune</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="560" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_docsearch_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_docsearch_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="docsearch" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_store_textfold" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="store" target="textfold"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_textfold_xnet" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="textfold" target="xnet"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tools_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agent_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_docsearch" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="docsearch"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_acp_tools" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="tools"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="agentinit" value="<b>internal/agentinit</b><br/><font style="font-size:10px">mm init: the agent.yaml wizard, presets.yaml, the server's model facts</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1640" y="660" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_main_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_config" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="config"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_elicit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="elicit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_agentinit_engine" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agentinit" target="engine"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_tui_agentinit" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="agentinit"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="wasmtool" value="<b>internal/wasmtool</b><br/><font style="font-size:10px">runs the wasm tools and hooks: JSON on stdin, declared folders only, timeout</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" vertex="1" parent="1"><mxGeometry x="1640" y="360" width="200" height="60" as="geometry" /></mxCell><mxCell id="wazero" value="<b>github.com/tetratelabs/wazero</b><br/><font style="font-size:10px">WebAssembly runtime in pure Go, WASI preview 1</font>" style="rounded=1;whiteSpace=wrap;html=1;dashed=1;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1"><mxGeometry x="1640" y="760" width="200" height="60" as="geometry" /></mxCell><mxCell id="e_tools_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tools" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_wasmtool_wazero" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1;" edge="1" parent="1" source="wasmtool" target="wazero"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="e_hooks_wasmtool" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="hooks" target="wasmtool"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="decide" value="<b>internal/decide</b><br/><font style="font-size:10px">decision models: /v1/systemone client, skill and tool routing</font>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="1140" y="840" width="200" height="60" as="geometry" /></mxCell><mxCell id="ed0" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="main" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed1" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="agent" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed2" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="acp" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed3" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="tui" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed4" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="engine" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed5" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="config" target="decide"><mxGeometry relative="1" as="geometry" /></mxCell><mxCell id="ed6" style="endArrow=block;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;" edge="1" parent="1" source="decide" target="skills"><mxGeometry relative="1" as="geometry" /></mxCell></root></mxGraphModel></diagram></mxfile> | ||
| \ No newline at end of file | \ No newline at end of file | ||
modified
docs/en/how-to/ask-a-decision-model.md +1 -1 | @@ -82,7 +82,7 @@ urgency: 1.89 → a careful review (confidence 0.83) — a careful review 0.92 | ||
| 82 | 82 | - **llmman or Ollama**: `provider: llmman` (or `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (or `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — pull it first. All three servers gave the same answers for the same request. |
| 83 | 83 | - **Another OpenAI-compatible server**: `provider: openai` and its `baseUrl`; mm posts to `/v1/systemone` at the server's root (`baseUrl` without its trailing `/v1`), and a server without the route answers 404. |
| 84 | 84 | - **From a sandbox**: add `fallback: http://host.docker.internal:8090/v1`. |
| 85 | -- **Before every question of a chat agent**: a `preGeneration` hook runs `mm -config agent.decider.yaml -p` on the question and returns the answers as a `context`, which the chat model reads under the question; its system prompt says what to do with them. `demos/decision-chat/` does this with a hook that turns any answer into a line, so the questions can change without touching it. | |
| 85 | +- **Before every question of a chat agent**: a `preGeneration` hook runs `mm -config agent.decider.yaml -p` on the question and returns the answers as a `context`, which the chat model reads under the question; its system prompt says what to do with them. `demos/decision-chat/` does this with a hook that turns any answer into a line, so the questions can change without touching it. To see the decider's answers yourself before the chat model answers, add `display: true` to the decider's `decision:` block: `-p` then marks its JSON, and the demos' hooks return the answers, indented, as the reply's `display` — mm shows them in their own colour (`demos/decision-dnd/` has it on). | |
| 86 | 86 | - **Without any chat model**: `demos/decision-only/` triages a folder of support messages with one `mm -p` call per message, read by a short Node.js script that prints a table, most urgent first. `demos/decision-reviews/` (customer reviews) and `demos/decision-moderation/` (blog comments) do the same and check every answer against a file of expected answers. |
| 87 | 87 | - **A smaller decision model**: `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (no Q4_K_M build is published). On the three demos above it was about five times faster than d1-3B (0.1 s per call against 0.55 s, `mm` start-up included) and less accurate: 26/28 against 28/28 on the reviews, 24/32 against 31/32 on the comments, five support categories out of six against six. |
| 88 | 88 | |
| @@ -82,7 +82,7 @@ urgency: 1.89 → a careful review (confidence 0.83) — a careful review 0.92 | |||
| 82 | - **llmman or Ollama**: `provider: llmman` (or `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (or `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — pull it first. All three servers gave the same answers for the same request. | 82 | - **llmman or Ollama**: `provider: llmman` (or `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (or `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — pull it first. All three servers gave the same answers for the same request. |
| 83 | - **Another OpenAI-compatible server**: `provider: openai` and its `baseUrl`; mm posts to `/v1/systemone` at the server's root (`baseUrl` without its trailing `/v1`), and a server without the route answers 404. | 83 | - **Another OpenAI-compatible server**: `provider: openai` and its `baseUrl`; mm posts to `/v1/systemone` at the server's root (`baseUrl` without its trailing `/v1`), and a server without the route answers 404. |
| 84 | - **From a sandbox**: add `fallback: http://host.docker.internal:8090/v1`. | 84 | - **From a sandbox**: add `fallback: http://host.docker.internal:8090/v1`. |
| 85 | -- **Before every question of a chat agent**: a `preGeneration` hook runs `mm -config agent.decider.yaml -p` on the question and returns the answers as a `context`, which the chat model reads under the question; its system prompt says what to do with them. `demos/decision-chat/` does this with a hook that turns any answer into a line, so the questions can change without touching it. | 85 | +- **Before every question of a chat agent**: a `preGeneration` hook runs `mm -config agent.decider.yaml -p` on the question and returns the answers as a `context`, which the chat model reads under the question; its system prompt says what to do with them. `demos/decision-chat/` does this with a hook that turns any answer into a line, so the questions can change without touching it. To see the decider's answers yourself before the chat model answers, add `display: true` to the decider's `decision:` block: `-p` then marks its JSON, and the demos' hooks return the answers, indented, as the reply's `display` — mm shows them in their own colour (`demos/decision-dnd/` has it on). |
| 86 | - **Without any chat model**: `demos/decision-only/` triages a folder of support messages with one `mm -p` call per message, read by a short Node.js script that prints a table, most urgent first. `demos/decision-reviews/` (customer reviews) and `demos/decision-moderation/` (blog comments) do the same and check every answer against a file of expected answers. | 86 | - **Without any chat model**: `demos/decision-only/` triages a folder of support messages with one `mm -p` call per message, read by a short Node.js script that prints a table, most urgent first. `demos/decision-reviews/` (customer reviews) and `demos/decision-moderation/` (blog comments) do the same and check every answer against a file of expected answers. |
| 87 | - **A smaller decision model**: `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (no Q4_K_M build is published). On the three demos above it was about five times faster than d1-3B (0.1 s per call against 0.55 s, `mm` start-up included) and less accurate: 26/28 against 28/28 on the reviews, 24/32 against 31/32 on the comments, five support categories out of six against six. | 87 | - **A smaller decision model**: `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (no Q4_K_M build is published). On the three demos above it was about five times faster than d1-3B (0.1 s per call against 0.55 s, `mm` start-up included) and less accurate: 26/28 against 28/28 on the reviews, 24/32 against 31/32 on the comments, five support categories out of six against six. |
| 88 | 88 | ||
modified
docs/en/reference/cli.md +1 -1 | @@ -137,7 +137,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 137 | 137 | | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. | |
| 138 | 138 | | Permissions | Nobody can answer. With `yolo: false`, every call that would ask is rejected (`[command rejected by the user]` for the model) and stderr says once: `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. MCP tools marked `ask` are rejected the same way, whatever `yolo` says. A call a `preToolCall` hook answered `allow` for runs: nobody had to be asked. | |
| 139 | 139 | | With `structuredOutput` | stdout carries the JSON answer and a newline, nothing else: no streamed text, no 🤖, no statistics line. Messages go to stderr. A failed or invalid answer prints `[structured output: <error>]` on stderr, nothing on stdout, and exits with status 1; a valid answer exits with status 0. See [`structuredOutput`](configuration.md#structuredoutput). | |
| 140 | -| With `decision` | stdout carries the server's JSON answer and a newline, nothing else. A failed answer prints `[decision: <error>]` on stderr, nothing on stdout, and exits with status 1. See [`decision`](configuration.md#decision). | | |
| 140 | +| With `decision` | stdout carries the server's JSON answer and a newline, nothing else; with `decision.display: true` the object starts with `"display":true`, added by mm. A failed answer prints `[decision: <error>]` on stderr, nothing on stdout, and exits with status 1. See [`decision`](configuration.md#decision). | | |
| 141 | 141 | | With `skillRouting` | No routing: `-p` sends the prompt as typed. | |
| 142 | 142 | | With `toolRouting` | Routed like any turn: the `🧰` notice goes to stdout before the answer. | |
| 143 | 143 | |
| @@ -137,7 +137,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 137 | | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. | | 137 | | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. | |
| 138 | | Permissions | Nobody can answer. With `yolo: false`, every call that would ask is rejected (`[command rejected by the user]` for the model) and stderr says once: `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. MCP tools marked `ask` are rejected the same way, whatever `yolo` says. A call a `preToolCall` hook answered `allow` for runs: nobody had to be asked. | | 138 | | Permissions | Nobody can answer. With `yolo: false`, every call that would ask is rejected (`[command rejected by the user]` for the model) and stderr says once: `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. MCP tools marked `ask` are rejected the same way, whatever `yolo` says. A call a `preToolCall` hook answered `allow` for runs: nobody had to be asked. | |
| 139 | | With `structuredOutput` | stdout carries the JSON answer and a newline, nothing else: no streamed text, no 🤖, no statistics line. Messages go to stderr. A failed or invalid answer prints `[structured output: <error>]` on stderr, nothing on stdout, and exits with status 1; a valid answer exits with status 0. See [`structuredOutput`](configuration.md#structuredoutput). | | 139 | | With `structuredOutput` | stdout carries the JSON answer and a newline, nothing else: no streamed text, no 🤖, no statistics line. Messages go to stderr. A failed or invalid answer prints `[structured output: <error>]` on stderr, nothing on stdout, and exits with status 1; a valid answer exits with status 0. See [`structuredOutput`](configuration.md#structuredoutput). | |
| 140 | -| With `decision` | stdout carries the server's JSON answer and a newline, nothing else. A failed answer prints `[decision: <error>]` on stderr, nothing on stdout, and exits with status 1. See [`decision`](configuration.md#decision). | | 140 | +| With `decision` | stdout carries the server's JSON answer and a newline, nothing else; with `decision.display: true` the object starts with `"display":true`, added by mm. A failed answer prints `[decision: <error>]` on stderr, nothing on stdout, and exits with status 1. See [`decision`](configuration.md#decision). | |
| 141 | | With `skillRouting` | No routing: `-p` sends the prompt as typed. | | 141 | | With `skillRouting` | No routing: `-p` sends the prompt as typed. | |
| 142 | | With `toolRouting` | Routed like any turn: the `🧰` notice goes to stdout before the answer. | | 142 | | With `toolRouting` | Routed like any turn: the `🧰` notice goes to stdout before the answer. | |
| 143 | 143 | ||
modified
docs/en/reference/configuration.md +1 -0 | @@ -248,6 +248,7 @@ structuredOutput: | | ||
| 248 | 248 | | Key | Type | Default | Description | |
| 249 | 249 | |-----|------|---------|-------------| |
| 250 | 250 | | `timeout` | duration | `60s` | Bound of one answer. Must be ≥ 0; `0` = the default. | |
| 251 | +| `display` | bool | `false` | `-p` adds `"display":true` at the head of the server's JSON, so a `preGeneration` hook that runs this decider can return the answers as its reply's `display`, shown to the user before the chat model answers (see [hooks](hooks.md#reply)). Nothing else changes. | | |
| 251 | 252 | | `questions` | mapping name → question | — | At least one. The order of the file is kept: in the request, in the answer lines, and for a choice's options. | |
| 252 | 253 | |
| 253 | 254 | A question: |
| @@ -248,6 +248,7 @@ structuredOutput: | | |||
| 248 | | Key | Type | Default | Description | | 248 | | Key | Type | Default | Description | |
| 249 | |-----|------|---------|-------------| | 249 | |-----|------|---------|-------------| |
| 250 | | `timeout` | duration | `60s` | Bound of one answer. Must be ≥ 0; `0` = the default. | | 250 | | `timeout` | duration | `60s` | Bound of one answer. Must be ≥ 0; `0` = the default. | |
| 251 | +| `display` | bool | `false` | `-p` adds `"display":true` at the head of the server's JSON, so a `preGeneration` hook that runs this decider can return the answers as its reply's `display`, shown to the user before the chat model answers (see [hooks](hooks.md#reply)). Nothing else changes. | | ||
| 251 | | `questions` | mapping name → question | — | At least one. The order of the file is kept: in the request, in the answer lines, and for a choice's options. | | 252 | | `questions` | mapping name → question | — | At least one. The order of the file is kept: in the request, in the answer lines, and for a choice's options. | |
| 252 | 253 | ||
| 253 | A question: | 254 | A question: |
modified
docs/en/reference/hooks.md +2 -0 | @@ -156,6 +156,7 @@ The JSON object a hook may write on stdout. **An empty stdout (or whitespace onl | ||
| 156 | 156 | | `transcript` | string | `preAsk` | replaces the conversation sent to the asked agent. Absent = unchanged; `""` = send none. | |
| 157 | 157 | | `answer` | string | `postAsk` | replaces the answer the asking agent reads. Absent = unchanged. | |
| 158 | 158 | | `context` | string | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | text for a model. `preGeneration`: appended to the question. `postToolCall`: appended to the tool's output. `preAsk`: appended to what the asked agent receives. `postAsk`: appended to the answer the asking agent reads. Each one is introduced by the line `[context added by a hook]`. Blank = ignored. | |
| 159 | +| `display` | string | `preGeneration` | text shown to the user, never sent to a model: before the answer, in its own colour (bright blue) under a `‣ hook` label in the window, in colour on a terminal (plain in a pipe), as a fenced block of the agent's message in an editor (tagged `json` when the text is JSON). Not kept in the history. Shown even when a later hook stops the turn. Blank = ignored. A hook that runs a decider whose file sets `decision.display: true` returns its answers here. | | |
| 159 | 160 | | `input` | any JSON value | `preToolCall` | replaces the tool call's arguments. Absent = unchanged. | |
| 160 | 161 | | `reason` | string | `preGeneration` (`stop`), `preToolCall` and `preAsk` (`deny`) | why. Ignored on `allow`: write it anyway, it documents the hook's verdict for whoever reads its output. | |
| 161 | 162 | | `response` | string | `preGeneration` (`stop`) | what the user reads instead of an answer. Absent or blank: `⛔ This request was blocked by a hook: <reason>`. | |
| @@ -182,6 +183,7 @@ Fields read on another event are ignored. | ||
| 182 | 183 | |----------|-----------------|-------------------|---------------| |
| 183 | 184 | | no change | the question | the question | — | |
| 184 | 185 | | `prompt` and/or `context` | the rewritten question, then each context | the rewritten question (the front end's own copy is not changed during the turn) | — | |
| 186 | +| `display` | — | — | each `display`, in hook order, before anything else of the turn | | |
| 185 | 187 | | `stop` | nothing: the model is not called | `[prompt blocked by a hook: <reason>]` (or `[prompt blocked by a hook]`) followed by the `response` as the answer | the `response`, as the answer | |
| 186 | 188 | |
| 187 | 189 | A chain: each hook reads the `prompt` the previous one left; contexts accumulate in hook order. |
| @@ -156,6 +156,7 @@ The JSON object a hook may write on stdout. **An empty stdout (or whitespace onl | |||
| 156 | | `transcript` | string | `preAsk` | replaces the conversation sent to the asked agent. Absent = unchanged; `""` = send none. | | 156 | | `transcript` | string | `preAsk` | replaces the conversation sent to the asked agent. Absent = unchanged; `""` = send none. | |
| 157 | | `answer` | string | `postAsk` | replaces the answer the asking agent reads. Absent = unchanged. | | 157 | | `answer` | string | `postAsk` | replaces the answer the asking agent reads. Absent = unchanged. | |
| 158 | | `context` | string | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | text for a model. `preGeneration`: appended to the question. `postToolCall`: appended to the tool's output. `preAsk`: appended to what the asked agent receives. `postAsk`: appended to the answer the asking agent reads. Each one is introduced by the line `[context added by a hook]`. Blank = ignored. | | 158 | | `context` | string | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | text for a model. `preGeneration`: appended to the question. `postToolCall`: appended to the tool's output. `preAsk`: appended to what the asked agent receives. `postAsk`: appended to the answer the asking agent reads. Each one is introduced by the line `[context added by a hook]`. Blank = ignored. | |
| 159 | +| `display` | string | `preGeneration` | text shown to the user, never sent to a model: before the answer, in its own colour (bright blue) under a `‣ hook` label in the window, in colour on a terminal (plain in a pipe), as a fenced block of the agent's message in an editor (tagged `json` when the text is JSON). Not kept in the history. Shown even when a later hook stops the turn. Blank = ignored. A hook that runs a decider whose file sets `decision.display: true` returns its answers here. | | ||
| 159 | | `input` | any JSON value | `preToolCall` | replaces the tool call's arguments. Absent = unchanged. | | 160 | | `input` | any JSON value | `preToolCall` | replaces the tool call's arguments. Absent = unchanged. | |
| 160 | | `reason` | string | `preGeneration` (`stop`), `preToolCall` and `preAsk` (`deny`) | why. Ignored on `allow`: write it anyway, it documents the hook's verdict for whoever reads its output. | | 161 | | `reason` | string | `preGeneration` (`stop`), `preToolCall` and `preAsk` (`deny`) | why. Ignored on `allow`: write it anyway, it documents the hook's verdict for whoever reads its output. | |
| 161 | | `response` | string | `preGeneration` (`stop`) | what the user reads instead of an answer. Absent or blank: `⛔ This request was blocked by a hook: <reason>`. | | 162 | | `response` | string | `preGeneration` (`stop`) | what the user reads instead of an answer. Absent or blank: `⛔ This request was blocked by a hook: <reason>`. | |
| @@ -182,6 +183,7 @@ Fields read on another event are ignored. | |||
| 182 | |----------|-----------------|-------------------|---------------| | 183 | |----------|-----------------|-------------------|---------------| |
| 183 | | no change | the question | the question | — | | 184 | | no change | the question | the question | — | |
| 184 | | `prompt` and/or `context` | the rewritten question, then each context | the rewritten question (the front end's own copy is not changed during the turn) | — | | 185 | | `prompt` and/or `context` | the rewritten question, then each context | the rewritten question (the front end's own copy is not changed during the turn) | — | |
| 186 | +| `display` | — | — | each `display`, in hook order, before anything else of the turn | | ||
| 185 | | `stop` | nothing: the model is not called | `[prompt blocked by a hook: <reason>]` (or `[prompt blocked by a hook]`) followed by the `response` as the answer | the `response`, as the answer | | 187 | | `stop` | nothing: the model is not called | `[prompt blocked by a hook: <reason>]` (or `[prompt blocked by a hook]`) followed by the `response` as the answer | the `response`, as the answer | |
| 186 | 188 | ||
| 187 | A chain: each hook reads the `prompt` the previous one left; contexts accumulate in hook order. | 189 | A chain: each hook reads the `prompt` the previous one left; contexts accumulate in hook order. |
modified
docs/fr/how-to/ask-a-decision-model.md +1 -1 | @@ -82,7 +82,7 @@ urgency: 1.89 → a careful review (confidence 0.83) — a careful review 0.92 | ||
| 82 | 82 | - **llmman ou Ollama** : `provider: llmman` (ou `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (ou `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — téléchargez-le d'abord. Les trois serveurs ont donné les mêmes réponses à la même requête. |
| 83 | 83 | - **Un autre serveur compatible OpenAI** : `provider: openai` et son `baseUrl` ; mm envoie à `/v1/systemone` à la racine du serveur (`baseUrl` sans son `/v1` final), et un serveur sans la route répond 404. |
| 84 | 84 | - **Depuis un sandbox** : ajoutez `fallback: http://host.docker.internal:8090/v1`. |
| 85 | -- **Avant chaque question d'un agent de chat** : un hook `preGeneration` lance `mm -config agent.decider.yaml -p` sur la question et renvoie les réponses en `context`, que le modèle de chat lit sous la question ; son prompt système dit quoi en faire. `demos/decision-chat/` le fait avec un hook qui transforme n'importe quelle réponse en ligne, si bien que les questions changent sans le toucher. | |
| 85 | +- **Avant chaque question d'un agent de chat** : un hook `preGeneration` lance `mm -config agent.decider.yaml -p` sur la question et renvoie les réponses en `context`, que le modèle de chat lit sous la question ; son prompt système dit quoi en faire. `demos/decision-chat/` le fait avec un hook qui transforme n'importe quelle réponse en ligne, si bien que les questions changent sans le toucher. Pour voir vous-même les réponses du décideur avant que le modèle de chat réponde, ajoutez `display: true` au bloc `decision:` du décideur : `-p` marque alors son JSON, et les hooks des démos renvoient les réponses, indentées, dans le `display` de leur réponse — mm les affiche dans leur propre couleur (`demos/decision-dnd/` l'active). | |
| 86 | 86 | - **Sans aucun modèle de chat** : `demos/decision-only/` trie un dossier de messages de support avec un appel `mm -p` par message, lu par un court script Node.js qui affiche un tableau, du plus urgent au moins urgent. `demos/decision-reviews/` (des avis clients) et `demos/decision-moderation/` (des commentaires de blog) font de même et vérifient chaque réponse par rapport à un fichier de réponses attendues. |
| 87 | 87 | - **Un modèle de décision plus petit** : `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (aucune version Q4_K_M n'est publiée). Sur les trois démos ci-dessus, il a été environ cinq fois plus rapide que d1-3B (0,1 s par appel contre 0,55 s, démarrage de `mm` compris) et moins juste : 26/28 contre 28/28 sur les avis, 24/32 contre 31/32 sur les commentaires, cinq catégories de support sur six contre six. |
| 88 | 88 | |
| @@ -82,7 +82,7 @@ urgency: 1.89 → a careful review (confidence 0.83) — a careful review 0.92 | |||
| 82 | - **llmman ou Ollama** : `provider: llmman` (ou `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (ou `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — téléchargez-le d'abord. Les trois serveurs ont donné les mêmes réponses à la même requête. | 82 | - **llmman ou Ollama** : `provider: llmman` (ou `ollama`), `baseUrl: http://127.0.0.1:17434/v1` (ou `:11434/v1`), `model: hf.co/LiquidAI/d1-3B-GGUF:Q4_K_M` — téléchargez-le d'abord. Les trois serveurs ont donné les mêmes réponses à la même requête. |
| 83 | - **Un autre serveur compatible OpenAI** : `provider: openai` et son `baseUrl` ; mm envoie à `/v1/systemone` à la racine du serveur (`baseUrl` sans son `/v1` final), et un serveur sans la route répond 404. | 83 | - **Un autre serveur compatible OpenAI** : `provider: openai` et son `baseUrl` ; mm envoie à `/v1/systemone` à la racine du serveur (`baseUrl` sans son `/v1` final), et un serveur sans la route répond 404. |
| 84 | - **Depuis un sandbox** : ajoutez `fallback: http://host.docker.internal:8090/v1`. | 84 | - **Depuis un sandbox** : ajoutez `fallback: http://host.docker.internal:8090/v1`. |
| 85 | -- **Avant chaque question d'un agent de chat** : un hook `preGeneration` lance `mm -config agent.decider.yaml -p` sur la question et renvoie les réponses en `context`, que le modèle de chat lit sous la question ; son prompt système dit quoi en faire. `demos/decision-chat/` le fait avec un hook qui transforme n'importe quelle réponse en ligne, si bien que les questions changent sans le toucher. | 85 | +- **Avant chaque question d'un agent de chat** : un hook `preGeneration` lance `mm -config agent.decider.yaml -p` sur la question et renvoie les réponses en `context`, que le modèle de chat lit sous la question ; son prompt système dit quoi en faire. `demos/decision-chat/` le fait avec un hook qui transforme n'importe quelle réponse en ligne, si bien que les questions changent sans le toucher. Pour voir vous-même les réponses du décideur avant que le modèle de chat réponde, ajoutez `display: true` au bloc `decision:` du décideur : `-p` marque alors son JSON, et les hooks des démos renvoient les réponses, indentées, dans le `display` de leur réponse — mm les affiche dans leur propre couleur (`demos/decision-dnd/` l'active). |
| 86 | - **Sans aucun modèle de chat** : `demos/decision-only/` trie un dossier de messages de support avec un appel `mm -p` par message, lu par un court script Node.js qui affiche un tableau, du plus urgent au moins urgent. `demos/decision-reviews/` (des avis clients) et `demos/decision-moderation/` (des commentaires de blog) font de même et vérifient chaque réponse par rapport à un fichier de réponses attendues. | 86 | - **Sans aucun modèle de chat** : `demos/decision-only/` trie un dossier de messages de support avec un appel `mm -p` par message, lu par un court script Node.js qui affiche un tableau, du plus urgent au moins urgent. `demos/decision-reviews/` (des avis clients) et `demos/decision-moderation/` (des commentaires de blog) font de même et vérifient chaque réponse par rapport à un fichier de réponses attendues. |
| 87 | - **Un modèle de décision plus petit** : `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (aucune version Q4_K_M n'est publiée). Sur les trois démos ci-dessus, il a été environ cinq fois plus rapide que d1-3B (0,1 s par appel contre 0,55 s, démarrage de `mm` compris) et moins juste : 26/28 contre 28/28 sur les avis, 24/32 contre 31/32 sur les commentaires, cinq catégories de support sur six contre six. | 87 | - **Un modèle de décision plus petit** : `model: hf.co/LiquidAI/d1-omni-600M-GGUF:Q8_0` (aucune version Q4_K_M n'est publiée). Sur les trois démos ci-dessus, il a été environ cinq fois plus rapide que d1-3B (0,1 s par appel contre 0,55 s, démarrage de `mm` compris) et moins juste : 26/28 contre 28/28 sur les avis, 24/32 contre 31/32 sur les commentaires, cinq catégories de support sur six contre six. |
| 88 | 88 | ||
modified
docs/fr/reference/cli.md +1 -1 | @@ -137,7 +137,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 137 | 137 | | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. | |
| 138 | 138 | | Permissions | Personne ne peut répondre. Avec `yolo: false`, chaque appel qui demanderait est refusé (`[command rejected by the user]` pour le modèle) et stderr le dit une fois : `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. Les outils MCP marqués `ask` sont refusés de la même façon, quoi que dise `yolo`. Un appel pour lequel un hook `preToolCall` a répondu `allow` s'exécute : personne n'avait à répondre. | |
| 139 | 139 | | Avec `structuredOutput` | stdout porte la réponse JSON et un saut de ligne, rien d'autre : pas de texte en flux, pas de 🤖, pas de ligne de statistiques. Les messages vont sur stderr. Une réponse en échec ou invalide imprime `[structured output: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1 ; une réponse valide sort avec le code 0. Voir [`structuredOutput`](configuration.md#structuredoutput). | |
| 140 | -| Avec `decision` | stdout porte la réponse JSON du serveur et un saut de ligne, rien d'autre. Une réponse en échec imprime `[decision: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1. Voir [`decision`](configuration.md#decision). | | |
| 140 | +| Avec `decision` | stdout porte la réponse JSON du serveur et un saut de ligne, rien d'autre ; avec `decision.display: true`, l'objet commence par `"display":true`, ajouté par mm. Une réponse en échec imprime `[decision: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1. Voir [`decision`](configuration.md#decision). | | |
| 141 | 141 | | Avec `skillRouting` | Pas de routage : `-p` envoie l'invite telle que tapée. | |
| 142 | 142 | | Avec `toolRouting` | Routé comme tout tour : la notice `🧰` part sur stdout avant la réponse. | |
| 143 | 143 | |
| @@ -137,7 +137,7 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 137 | | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. | | 137 | | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. | |
| 138 | | Permissions | Personne ne peut répondre. Avec `yolo: false`, chaque appel qui demanderait est refusé (`[command rejected by the user]` pour le modèle) et stderr le dit une fois : `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. Les outils MCP marqués `ask` sont refusés de la même façon, quoi que dise `yolo`. Un appel pour lequel un hook `preToolCall` a répondu `allow` s'exécute : personne n'avait à répondre. | | 138 | | Permissions | Personne ne peut répondre. Avec `yolo: false`, chaque appel qui demanderait est refusé (`[command rejected by the user]` pour le modèle) et stderr le dit une fois : `[yolo is off and this mode cannot ask: tool calls that need a confirmation are rejected — set yolo: true to let them run]`. Les outils MCP marqués `ask` sont refusés de la même façon, quoi que dise `yolo`. Un appel pour lequel un hook `preToolCall` a répondu `allow` s'exécute : personne n'avait à répondre. | |
| 139 | | Avec `structuredOutput` | stdout porte la réponse JSON et un saut de ligne, rien d'autre : pas de texte en flux, pas de 🤖, pas de ligne de statistiques. Les messages vont sur stderr. Une réponse en échec ou invalide imprime `[structured output: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1 ; une réponse valide sort avec le code 0. Voir [`structuredOutput`](configuration.md#structuredoutput). | | 139 | | Avec `structuredOutput` | stdout porte la réponse JSON et un saut de ligne, rien d'autre : pas de texte en flux, pas de 🤖, pas de ligne de statistiques. Les messages vont sur stderr. Une réponse en échec ou invalide imprime `[structured output: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1 ; une réponse valide sort avec le code 0. Voir [`structuredOutput`](configuration.md#structuredoutput). | |
| 140 | -| Avec `decision` | stdout porte la réponse JSON du serveur et un saut de ligne, rien d'autre. Une réponse en échec imprime `[decision: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1. Voir [`decision`](configuration.md#decision). | | 140 | +| Avec `decision` | stdout porte la réponse JSON du serveur et un saut de ligne, rien d'autre ; avec `decision.display: true`, l'objet commence par `"display":true`, ajouté par mm. Une réponse en échec imprime `[decision: <erreur>]` sur stderr, rien sur stdout, et sort avec le code 1. Voir [`decision`](configuration.md#decision). | |
| 141 | | Avec `skillRouting` | Pas de routage : `-p` envoie l'invite telle que tapée. | | 141 | | Avec `skillRouting` | Pas de routage : `-p` envoie l'invite telle que tapée. | |
| 142 | | Avec `toolRouting` | Routé comme tout tour : la notice `🧰` part sur stdout avant la réponse. | | 142 | | Avec `toolRouting` | Routé comme tout tour : la notice `🧰` part sur stdout avant la réponse. | |
| 143 | 143 | ||
modified
docs/fr/reference/configuration.md +1 -0 | @@ -248,6 +248,7 @@ structuredOutput: | | ||
| 248 | 248 | | Clé | Type | Défaut | Description | |
| 249 | 249 | |-----|------|--------|-------------| |
| 250 | 250 | | `timeout` | durée | `60s` | Borne d'une réponse. Doit être ≥ 0 ; `0` = la valeur par défaut. | |
| 251 | +| `display` | booléen | `false` | `-p` ajoute `"display":true` en tête du JSON du serveur, pour qu'un hook `preGeneration` qui lance ce décideur puisse renvoyer les réponses dans le `display` de sa réponse, montré à l'utilisateur avant que le modèle de chat réponde (voir [hooks](hooks.md#réponse)). Rien d'autre ne change. | | |
| 251 | 252 | | `questions` | mapping nom → question | — | Au moins une. L'ordre du fichier est conservé : dans la requête, dans les lignes de réponse, et pour les options d'un choix. | |
| 252 | 253 | |
| 253 | 254 | Une question : |
| @@ -248,6 +248,7 @@ structuredOutput: | | |||
| 248 | | Clé | Type | Défaut | Description | | 248 | | Clé | Type | Défaut | Description | |
| 249 | |-----|------|--------|-------------| | 249 | |-----|------|--------|-------------| |
| 250 | | `timeout` | durée | `60s` | Borne d'une réponse. Doit être ≥ 0 ; `0` = la valeur par défaut. | | 250 | | `timeout` | durée | `60s` | Borne d'une réponse. Doit être ≥ 0 ; `0` = la valeur par défaut. | |
| 251 | +| `display` | booléen | `false` | `-p` ajoute `"display":true` en tête du JSON du serveur, pour qu'un hook `preGeneration` qui lance ce décideur puisse renvoyer les réponses dans le `display` de sa réponse, montré à l'utilisateur avant que le modèle de chat réponde (voir [hooks](hooks.md#réponse)). Rien d'autre ne change. | | ||
| 251 | | `questions` | mapping nom → question | — | Au moins une. L'ordre du fichier est conservé : dans la requête, dans les lignes de réponse, et pour les options d'un choix. | | 252 | | `questions` | mapping nom → question | — | Au moins une. L'ordre du fichier est conservé : dans la requête, dans les lignes de réponse, et pour les options d'un choix. | |
| 252 | 253 | ||
| 253 | Une question : | 254 | Une question : |
modified
docs/fr/reference/hooks.md +2 -0 | @@ -156,6 +156,7 @@ L'objet JSON qu'un hook peut écrire sur sa sortie standard. **Une sortie vide ( | ||
| 156 | 156 | | `transcript` | chaîne | `preAsk` | remplace la conversation envoyée à l'agent appelé. Absent = inchangée ; `""` = n'en envoyer aucune. | |
| 157 | 157 | | `answer` | chaîne | `postAsk` | remplace la réponse que lit l'agent qui appelle. Absent = inchangée. | |
| 158 | 158 | | `context` | chaîne | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | du texte pour un modèle. `preGeneration` : ajouté après la question. `postToolCall` : ajouté après la sortie de l'outil. `preAsk` : ajouté à ce que reçoit l'agent appelé. `postAsk` : ajouté après la réponse que lit l'agent qui appelle. Chacun est introduit par la ligne `[context added by a hook]`. Blanc = ignoré. | |
| 159 | +| `display` | chaîne | `preGeneration` | du texte montré à l'utilisateur, jamais envoyé à un modèle : avant la réponse, dans sa propre couleur (bleu vif) sous une étiquette `‣ hook` dans la fenêtre, en couleur dans un terminal (sans couleur dans un pipe), en bloc délimité du message de l'agent dans un éditeur (marqué `json` quand le texte est du JSON). Pas gardé dans l'historique. Montré même quand un hook suivant arrête le tour. Blanc = ignoré. Un hook qui lance un décideur dont le fichier pose `decision.display: true` y renvoie ses réponses. | | |
| 159 | 160 | | `input` | toute valeur JSON | `preToolCall` | remplace les arguments de l'appel d'outil. Absent = inchangés. | |
| 160 | 161 | | `reason` | chaîne | `preGeneration` (`stop`), `preToolCall` et `preAsk` (`deny`) | la raison. Ignorée sur `allow` : écrivez-la quand même, elle documente le verdict du hook pour qui lit sa sortie. | |
| 161 | 162 | | `response` | chaîne | `preGeneration` (`stop`) | ce que l'utilisateur lit à la place d'une réponse. Absente ou blanche : `⛔ This request was blocked by a hook: <raison>`. | |
| @@ -182,6 +183,7 @@ Les champs lus sur un autre événement sont ignorés. | ||
| 182 | 183 | |----------|---------------|--------------------|--------------------| |
| 183 | 184 | | aucun changement | la question | la question | — | |
| 184 | 185 | | `prompt` et/ou `context` | la question réécrite, puis chaque contexte | la question réécrite (la copie propre au front end n'est pas modifiée pendant le tour) | — | |
| 186 | +| `display` | — | — | chaque `display`, dans l'ordre des hooks, avant tout le reste du tour | | |
| 185 | 187 | | `stop` | rien : le modèle n'est pas appelé | `[prompt blocked by a hook: <raison>]` (ou `[prompt blocked by a hook]`) suivi de la `response` comme réponse | la `response`, comme réponse | |
| 186 | 188 | |
| 187 | 189 | En chaîne : chaque hook lit le `prompt` laissé par le précédent ; les contextes s'accumulent dans l'ordre des hooks. |
| @@ -156,6 +156,7 @@ L'objet JSON qu'un hook peut écrire sur sa sortie standard. **Une sortie vide ( | |||
| 156 | | `transcript` | chaîne | `preAsk` | remplace la conversation envoyée à l'agent appelé. Absent = inchangée ; `""` = n'en envoyer aucune. | | 156 | | `transcript` | chaîne | `preAsk` | remplace la conversation envoyée à l'agent appelé. Absent = inchangée ; `""` = n'en envoyer aucune. | |
| 157 | | `answer` | chaîne | `postAsk` | remplace la réponse que lit l'agent qui appelle. Absent = inchangée. | | 157 | | `answer` | chaîne | `postAsk` | remplace la réponse que lit l'agent qui appelle. Absent = inchangée. | |
| 158 | | `context` | chaîne | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | du texte pour un modèle. `preGeneration` : ajouté après la question. `postToolCall` : ajouté après la sortie de l'outil. `preAsk` : ajouté à ce que reçoit l'agent appelé. `postAsk` : ajouté après la réponse que lit l'agent qui appelle. Chacun est introduit par la ligne `[context added by a hook]`. Blanc = ignoré. | | 158 | | `context` | chaîne | `preGeneration`, `postToolCall`, `preAsk`, `postAsk` | du texte pour un modèle. `preGeneration` : ajouté après la question. `postToolCall` : ajouté après la sortie de l'outil. `preAsk` : ajouté à ce que reçoit l'agent appelé. `postAsk` : ajouté après la réponse que lit l'agent qui appelle. Chacun est introduit par la ligne `[context added by a hook]`. Blanc = ignoré. | |
| 159 | +| `display` | chaîne | `preGeneration` | du texte montré à l'utilisateur, jamais envoyé à un modèle : avant la réponse, dans sa propre couleur (bleu vif) sous une étiquette `‣ hook` dans la fenêtre, en couleur dans un terminal (sans couleur dans un pipe), en bloc délimité du message de l'agent dans un éditeur (marqué `json` quand le texte est du JSON). Pas gardé dans l'historique. Montré même quand un hook suivant arrête le tour. Blanc = ignoré. Un hook qui lance un décideur dont le fichier pose `decision.display: true` y renvoie ses réponses. | | ||
| 159 | | `input` | toute valeur JSON | `preToolCall` | remplace les arguments de l'appel d'outil. Absent = inchangés. | | 160 | | `input` | toute valeur JSON | `preToolCall` | remplace les arguments de l'appel d'outil. Absent = inchangés. | |
| 160 | | `reason` | chaîne | `preGeneration` (`stop`), `preToolCall` et `preAsk` (`deny`) | la raison. Ignorée sur `allow` : écrivez-la quand même, elle documente le verdict du hook pour qui lit sa sortie. | | 161 | | `reason` | chaîne | `preGeneration` (`stop`), `preToolCall` et `preAsk` (`deny`) | la raison. Ignorée sur `allow` : écrivez-la quand même, elle documente le verdict du hook pour qui lit sa sortie. | |
| 161 | | `response` | chaîne | `preGeneration` (`stop`) | ce que l'utilisateur lit à la place d'une réponse. Absente ou blanche : `⛔ This request was blocked by a hook: <raison>`. | | 162 | | `response` | chaîne | `preGeneration` (`stop`) | ce que l'utilisateur lit à la place d'une réponse. Absente ou blanche : `⛔ This request was blocked by a hook: <raison>`. | |
| @@ -182,6 +183,7 @@ Les champs lus sur un autre événement sont ignorés. | |||
| 182 | |----------|---------------|--------------------|--------------------| | 183 | |----------|---------------|--------------------|--------------------| |
| 183 | | aucun changement | la question | la question | — | | 184 | | aucun changement | la question | la question | — | |
| 184 | | `prompt` et/ou `context` | la question réécrite, puis chaque contexte | la question réécrite (la copie propre au front end n'est pas modifiée pendant le tour) | — | | 185 | | `prompt` et/ou `context` | la question réécrite, puis chaque contexte | la question réécrite (la copie propre au front end n'est pas modifiée pendant le tour) | — | |
| 186 | +| `display` | — | — | chaque `display`, dans l'ordre des hooks, avant tout le reste du tour | | ||
| 185 | | `stop` | rien : le modèle n'est pas appelé | `[prompt blocked by a hook: <raison>]` (ou `[prompt blocked by a hook]`) suivi de la `response` comme réponse | la `response`, comme réponse | | 187 | | `stop` | rien : le modèle n'est pas appelé | `[prompt blocked by a hook: <raison>]` (ou `[prompt blocked by a hook]`) suivi de la `response` comme réponse | la `response`, comme réponse | |
| 186 | 188 | ||
| 187 | En chaîne : chaque hook lit le `prompt` laissé par le précédent ; les contextes s'accumulent dans l'ordre des hooks. | 189 | En chaîne : chaque hook lit le `prompt` laissé par le précédent ; les contextes s'accumulent dans l'ordre des hooks. |
added
internal/acp/display_test.go +17 -0 | new file mode 100644 | ||
| @@ -0,0 +1,17 @@ | ||
| 1 | +package acp | |
| 2 | + | |
| 3 | +import "testing" | |
| 4 | + | |
| 5 | +// A hook's display reaches the editor as a fenced block before the answer: | |
| 6 | +// tagged json when it parses, plain otherwise. | |
| 7 | +func TestDisplayBlockFencesTheText(t *testing.T) { | |
| 8 | + cases := map[string]string{ | |
| 9 | + "{\n \"spell\": \"sleep\"\n}\n": "```json\n{\n \"spell\": \"sleep\"\n}\n```\n\n", | |
| 10 | + "spell: sleep": "```\nspell: sleep\n```\n\n", | |
| 11 | + } | |
| 12 | + for text, want := range cases { | |
| 13 | + if got := displayBlock(text); got != want { | |
| 14 | + t.Errorf("displayBlock(%q) = %q, want %q", text, got, want) | |
| 15 | + } | |
| 16 | + } | |
| 17 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | +package acp | ||
| 2 | + | ||
| 3 | +import "testing" | ||
| 4 | + | ||
| 5 | +// A hook's display reaches the editor as a fenced block before the answer: | ||
| 6 | +// tagged json when it parses, plain otherwise. | ||
| 7 | +func TestDisplayBlockFencesTheText(t *testing.T) { | ||
| 8 | + cases := map[string]string{ | ||
| 9 | + "{\n \"spell\": \"sleep\"\n}\n": "```json\n{\n \"spell\": \"sleep\"\n}\n```\n\n", | ||
| 10 | + "spell: sleep": "```\nspell: sleep\n```\n\n", | ||
| 11 | + } | ||
| 12 | + for text, want := range cases { | ||
| 13 | + if got := displayBlock(text); got != want { | ||
| 14 | + t.Errorf("displayBlock(%q) = %q, want %q", text, got, want) | ||
| 15 | + } | ||
| 16 | + } | ||
| 17 | +} | ||
modified
internal/acp/sink.go +22 -0 | @@ -2,6 +2,8 @@ package acp | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "context" |
| 5 | + "encoding/json" | |
| 6 | + "strings" | |
| 5 | 7 | |
| 6 | 8 | "mm/internal/config" |
| 7 | 9 | "mm/internal/consent" |
| @@ -37,6 +39,26 @@ func (k *sink) Text(text string) { | ||
| 37 | 39 | k.send(sdk.UpdateAgentMessageText(text)) |
| 38 | 40 | } |
| 39 | 41 | |
| 42 | +// Display shows what a hook asked to show the user (a decider's answer) | |
| 43 | +// before the answer streams. An editor has no colour of ours to give it, so | |
| 44 | +// it travels as a fenced block of the agent's message — never in the | |
| 45 | +// history: a resumed session does not replay it. | |
| 46 | +func (k *sink) Display(text string) { | |
| 47 | + k.send(sdk.UpdateAgentMessageText(displayBlock(text))) | |
| 48 | +} | |
| 49 | + | |
| 50 | +// displayBlock fences a hook's display for the editor's Markdown: tagged | |
| 51 | +// json when it is JSON, so the editor highlights it. | |
| 52 | +// | |
| 53 | +// displayBlock(`{"spell":"sleep"}`) // "```json\n{\"spell\":\"sleep\"}\n```\n\n" | |
| 54 | +func displayBlock(text string) string { | |
| 55 | + fence := "```" | |
| 56 | + if json.Valid([]byte(text)) { | |
| 57 | + fence += "json" | |
| 58 | + } | |
| 59 | + return fence + "\n" + strings.TrimRight(text, "\n") + "\n```\n\n" | |
| 60 | +} | |
| 61 | + | |
| 40 | 62 | // Reasoning streams what a thinking model produced before its answer. ACP |
| 41 | 63 | // has the concept, so this is not a rendering trick: agent_thought_chunk is |
| 42 | 64 | // a separate update and the editor shows it its own way — Zed folds it under |
| @@ -2,6 +2,8 @@ package acp | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| 5 | + "encoding/json" | ||
| 6 | + "strings" | ||
| 5 | 7 | ||
| 6 | "mm/internal/config" | 8 | "mm/internal/config" |
| 7 | "mm/internal/consent" | 9 | "mm/internal/consent" |
| @@ -37,6 +39,26 @@ func (k *sink) Text(text string) { | |||
| 37 | k.send(sdk.UpdateAgentMessageText(text)) | 39 | k.send(sdk.UpdateAgentMessageText(text)) |
| 38 | } | 40 | } |
| 39 | 41 | ||
| 42 | +// Display shows what a hook asked to show the user (a decider's answer) | ||
| 43 | +// before the answer streams. An editor has no colour of ours to give it, so | ||
| 44 | +// it travels as a fenced block of the agent's message — never in the | ||
| 45 | +// history: a resumed session does not replay it. | ||
| 46 | +func (k *sink) Display(text string) { | ||
| 47 | + k.send(sdk.UpdateAgentMessageText(displayBlock(text))) | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | +// displayBlock fences a hook's display for the editor's Markdown: tagged | ||
| 51 | +// json when it is JSON, so the editor highlights it. | ||
| 52 | +// | ||
| 53 | +// displayBlock(`{"spell":"sleep"}`) // "```json\n{\"spell\":\"sleep\"}\n```\n\n" | ||
| 54 | +func displayBlock(text string) string { | ||
| 55 | + fence := "```" | ||
| 56 | + if json.Valid([]byte(text)) { | ||
| 57 | + fence += "json" | ||
| 58 | + } | ||
| 59 | + return fence + "\n" + strings.TrimRight(text, "\n") + "\n```\n\n" | ||
| 60 | +} | ||
| 61 | + | ||
| 40 | // Reasoning streams what a thinking model produced before its answer. ACP | 62 | // Reasoning streams what a thinking model produced before its answer. ACP |
| 41 | // has the concept, so this is not a rendering trick: agent_thought_chunk is | 63 | // has the concept, so this is not a rendering trick: agent_thought_chunk is |
| 42 | // a separate update and the editor shows it its own way — Zed folds it under | 64 | // a separate update and the editor shows it its own way — Zed folds it under |
modified
internal/config/decision.go +14 -3 | @@ -35,6 +35,7 @@ const DefaultDecisionTimeout = 60 * time.Second | ||
| 35 | 35 | // |
| 36 | 36 | // decision: |
| 37 | 37 | // timeout: 60s |
| 38 | +// display: true # -p marks its JSON: the chat shows it (hooks) | |
| 38 | 39 | // questions: |
| 39 | 40 | // risk: |
| 40 | 41 | // type: choice |
| @@ -53,7 +54,13 @@ const DefaultDecisionTimeout = 60 * time.Second | ||
| 53 | 54 | // Questions keep the order of the file: the answer lines follow it, and a |
| 54 | 55 | // choice's options reach the model in the order they are written. |
| 55 | 56 | type DecisionConfig struct { |
| 56 | - Timeout time.Duration | |
| 57 | + Timeout time.Duration | |
| 58 | + // Display asks the chat agent that consults this decider to show its | |
| 59 | + // answer: `-p` then adds `"display": true` at the head of the server's | |
| 60 | + // JSON, and a preGeneration hook that sees it hands the answer back as | |
| 61 | + // the reply's `display` (ticket 141 follow-up). Off by default: the JSON | |
| 62 | + // stays the server's, byte for byte. | |
| 63 | + Display bool | |
| 57 | 64 | Questions []decide.Question |
| 58 | 65 | } |
| 59 | 66 | |
| @@ -69,7 +76,7 @@ func (d DecisionConfig) Wait() time.Duration { | ||
| 69 | 76 | // the questions and of a choice's options. |
| 70 | 77 | func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error { |
| 71 | 78 | if node.Kind != yaml.MappingNode { |
| 72 | - return fmt.Errorf("line %d: decision must be a mapping (timeout, questions)", node.Line) | |
| 79 | + return fmt.Errorf("line %d: decision must be a mapping (timeout, display, questions)", node.Line) | |
| 73 | 80 | } |
| 74 | 81 | for i := 0; i+1 < len(node.Content); i += 2 { |
| 75 | 82 | key, value := node.Content[i], node.Content[i+1] |
| @@ -78,6 +85,10 @@ func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error { | ||
| 78 | 85 | if err := value.Decode(&d.Timeout); err != nil { |
| 79 | 86 | return fmt.Errorf("line %d: decision.timeout: %w", value.Line, err) |
| 80 | 87 | } |
| 88 | + case "display": | |
| 89 | + if err := value.Decode(&d.Display); err != nil { | |
| 90 | + return fmt.Errorf("line %d: decision.display: %w", value.Line, err) | |
| 91 | + } | |
| 81 | 92 | case "questions": |
| 82 | 93 | questions, err := decodeQuestions(value) |
| 83 | 94 | if err != nil { |
| @@ -85,7 +96,7 @@ func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error { | ||
| 85 | 96 | } |
| 86 | 97 | d.Questions = questions |
| 87 | 98 | default: |
| 88 | - return fmt.Errorf("line %d: decision: unknown key %q (timeout, questions)", key.Line, key.Value) | |
| 99 | + return fmt.Errorf("line %d: decision: unknown key %q (timeout, display, questions)", key.Line, key.Value) | |
| 89 | 100 | } |
| 90 | 101 | } |
| 91 | 102 | return nil |
| @@ -35,6 +35,7 @@ const DefaultDecisionTimeout = 60 * time.Second | |||
| 35 | // | 35 | // |
| 36 | // decision: | 36 | // decision: |
| 37 | // timeout: 60s | 37 | // timeout: 60s |
| 38 | +// display: true # -p marks its JSON: the chat shows it (hooks) | ||
| 38 | // questions: | 39 | // questions: |
| 39 | // risk: | 40 | // risk: |
| 40 | // type: choice | 41 | // type: choice |
| @@ -53,7 +54,13 @@ const DefaultDecisionTimeout = 60 * time.Second | |||
| 53 | // Questions keep the order of the file: the answer lines follow it, and a | 54 | // Questions keep the order of the file: the answer lines follow it, and a |
| 54 | // choice's options reach the model in the order they are written. | 55 | // choice's options reach the model in the order they are written. |
| 55 | type DecisionConfig struct { | 56 | type DecisionConfig struct { |
| 56 | - Timeout time.Duration | 57 | + Timeout time.Duration |
| 58 | + // Display asks the chat agent that consults this decider to show its | ||
| 59 | + // answer: `-p` then adds `"display": true` at the head of the server's | ||
| 60 | + // JSON, and a preGeneration hook that sees it hands the answer back as | ||
| 61 | + // the reply's `display` (ticket 141 follow-up). Off by default: the JSON | ||
| 62 | + // stays the server's, byte for byte. | ||
| 63 | + Display bool | ||
| 57 | Questions []decide.Question | 64 | Questions []decide.Question |
| 58 | } | 65 | } |
| 59 | 66 | ||
| @@ -69,7 +76,7 @@ func (d DecisionConfig) Wait() time.Duration { | |||
| 69 | // the questions and of a choice's options. | 76 | // the questions and of a choice's options. |
| 70 | func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error { | 77 | func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error { |
| 71 | if node.Kind != yaml.MappingNode { | 78 | if node.Kind != yaml.MappingNode { |
| 72 | - return fmt.Errorf("line %d: decision must be a mapping (timeout, questions)", node.Line) | 79 | + return fmt.Errorf("line %d: decision must be a mapping (timeout, display, questions)", node.Line) |
| 73 | } | 80 | } |
| 74 | for i := 0; i+1 < len(node.Content); i += 2 { | 81 | for i := 0; i+1 < len(node.Content); i += 2 { |
| 75 | key, value := node.Content[i], node.Content[i+1] | 82 | key, value := node.Content[i], node.Content[i+1] |
| @@ -78,6 +85,10 @@ func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error { | |||
| 78 | if err := value.Decode(&d.Timeout); err != nil { | 85 | if err := value.Decode(&d.Timeout); err != nil { |
| 79 | return fmt.Errorf("line %d: decision.timeout: %w", value.Line, err) | 86 | return fmt.Errorf("line %d: decision.timeout: %w", value.Line, err) |
| 80 | } | 87 | } |
| 88 | + case "display": | ||
| 89 | + if err := value.Decode(&d.Display); err != nil { | ||
| 90 | + return fmt.Errorf("line %d: decision.display: %w", value.Line, err) | ||
| 91 | + } | ||
| 81 | case "questions": | 92 | case "questions": |
| 82 | questions, err := decodeQuestions(value) | 93 | questions, err := decodeQuestions(value) |
| 83 | if err != nil { | 94 | if err != nil { |
| @@ -85,7 +96,7 @@ func (d *DecisionConfig) UnmarshalYAML(node *yaml.Node) error { | |||
| 85 | } | 96 | } |
| 86 | d.Questions = questions | 97 | d.Questions = questions |
| 87 | default: | 98 | default: |
| 88 | - return fmt.Errorf("line %d: decision: unknown key %q (timeout, questions)", key.Line, key.Value) | 99 | + return fmt.Errorf("line %d: decision: unknown key %q (timeout, display, questions)", key.Line, key.Value) |
| 89 | } | 100 | } |
| 90 | } | 101 | } |
| 91 | return nil | 102 | return nil |
modified
internal/config/decision_test.go +17 -0 | @@ -58,6 +58,22 @@ func TestTheDecisionBlockKeepsTheOrderOfQuestionsAndOptions(t *testing.T) { | ||
| 58 | 58 | } |
| 59 | 59 | } |
| 60 | 60 | |
| 61 | +func TestDecisionDisplayIsOffUnlessTheFileSetsIt(t *testing.T) { | |
| 62 | + if err := loadYAML(t, deciderYAML); err != nil { | |
| 63 | + t.Fatal(err) | |
| 64 | + } | |
| 65 | + if Cfg.Decision.Display { | |
| 66 | + t.Error("display is on without the key") | |
| 67 | + } | |
| 68 | + body := strings.Replace(deciderYAML, "decision:\n", "decision:\n display: true\n", 1) | |
| 69 | + if err := loadYAML(t, body); err != nil { | |
| 70 | + t.Fatal(err) | |
| 71 | + } | |
| 72 | + if !Cfg.Decision.Display || len(Cfg.Decision.Questions) != 3 { | |
| 73 | + t.Errorf("Decision = %+v", Cfg.Decision) | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 61 | 77 | func TestAChatAgentHasNoDecisionBlock(t *testing.T) { |
| 62 | 78 | if err := loadYAML(t, "model: m\n"); err != nil { |
| 63 | 79 | t.Fatal(err) |
| @@ -84,6 +100,7 @@ func TestInvalidDecisionBlocksAreConfigErrors(t *testing.T) { | ||
| 84 | 100 | "levels map": {head + "decision:\n questions:\n q: {type: score, instructions: x, criteria: {a: A, b: B}}\n", "2 to 10 levels"}, |
| 85 | 101 | "choice list": {head + "decision:\n questions:\n q: {type: choice, instructions: x, criteria: [a, b]}\n", "at least 2 options"}, |
| 86 | 102 | "noul criteria": {head + "decision:\n questions:\n q: {type: noul, instructions: x, criteria: [a, b]}\n", "takes no criteria"}, |
| 103 | + "display text": {head + "decision:\n display: sometimes\n questions:\n q: {type: noul, instructions: x}\n", "decision.display"}, | |
| 87 | 104 | "dmr": {"provider: dmr\nmodel: m\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "Docker Model Runner"}, |
| 88 | 105 | "structured too": {head + "structuredOutput: |\n type A struct {\n B bool `json:\"b\"`\n }\n var X A\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "cannot be used together"}, |
| 89 | 106 | } |
| @@ -58,6 +58,22 @@ func TestTheDecisionBlockKeepsTheOrderOfQuestionsAndOptions(t *testing.T) { | |||
| 58 | } | 58 | } |
| 59 | } | 59 | } |
| 60 | 60 | ||
| 61 | +func TestDecisionDisplayIsOffUnlessTheFileSetsIt(t *testing.T) { | ||
| 62 | + if err := loadYAML(t, deciderYAML); err != nil { | ||
| 63 | + t.Fatal(err) | ||
| 64 | + } | ||
| 65 | + if Cfg.Decision.Display { | ||
| 66 | + t.Error("display is on without the key") | ||
| 67 | + } | ||
| 68 | + body := strings.Replace(deciderYAML, "decision:\n", "decision:\n display: true\n", 1) | ||
| 69 | + if err := loadYAML(t, body); err != nil { | ||
| 70 | + t.Fatal(err) | ||
| 71 | + } | ||
| 72 | + if !Cfg.Decision.Display || len(Cfg.Decision.Questions) != 3 { | ||
| 73 | + t.Errorf("Decision = %+v", Cfg.Decision) | ||
| 74 | + } | ||
| 75 | +} | ||
| 76 | + | ||
| 61 | func TestAChatAgentHasNoDecisionBlock(t *testing.T) { | 77 | func TestAChatAgentHasNoDecisionBlock(t *testing.T) { |
| 62 | if err := loadYAML(t, "model: m\n"); err != nil { | 78 | if err := loadYAML(t, "model: m\n"); err != nil { |
| 63 | t.Fatal(err) | 79 | t.Fatal(err) |
| @@ -84,6 +100,7 @@ func TestInvalidDecisionBlocksAreConfigErrors(t *testing.T) { | |||
| 84 | "levels map": {head + "decision:\n questions:\n q: {type: score, instructions: x, criteria: {a: A, b: B}}\n", "2 to 10 levels"}, | 100 | "levels map": {head + "decision:\n questions:\n q: {type: score, instructions: x, criteria: {a: A, b: B}}\n", "2 to 10 levels"}, |
| 85 | "choice list": {head + "decision:\n questions:\n q: {type: choice, instructions: x, criteria: [a, b]}\n", "at least 2 options"}, | 101 | "choice list": {head + "decision:\n questions:\n q: {type: choice, instructions: x, criteria: [a, b]}\n", "at least 2 options"}, |
| 86 | "noul criteria": {head + "decision:\n questions:\n q: {type: noul, instructions: x, criteria: [a, b]}\n", "takes no criteria"}, | 102 | "noul criteria": {head + "decision:\n questions:\n q: {type: noul, instructions: x, criteria: [a, b]}\n", "takes no criteria"}, |
| 103 | + "display text": {head + "decision:\n display: sometimes\n questions:\n q: {type: noul, instructions: x}\n", "decision.display"}, | ||
| 87 | "dmr": {"provider: dmr\nmodel: m\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "Docker Model Runner"}, | 104 | "dmr": {"provider: dmr\nmodel: m\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "Docker Model Runner"}, |
| 88 | "structured too": {head + "structuredOutput: |\n type A struct {\n B bool `json:\"b\"`\n }\n var X A\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "cannot be used together"}, | 105 | "structured too": {head + "structuredOutput: |\n type A struct {\n B bool `json:\"b\"`\n }\n var X A\ndecision:\n questions:\n q: {type: noul, instructions: x}\n", "cannot be used together"}, |
| 89 | } | 106 | } |
modified
internal/decide/decide.go +21 -0 | @@ -365,3 +365,24 @@ func likeliest(keys, labels []string, probabilities map[string]float64) string { | ||
| 365 | 365 | } |
| 366 | 366 | return labels[best] |
| 367 | 367 | } |
| 368 | + | |
| 369 | +// MarkDisplay returns the server's JSON object with `"display":true` added | |
| 370 | +// at its head — what `-p` prints for a decider whose file sets | |
| 371 | +// `decision.display: true`, so the hook that runs it knows the chat should | |
| 372 | +// show the answer. Everything else is left byte for byte; a payload that is | |
| 373 | +// not an object is returned unchanged. | |
| 374 | +// | |
| 375 | +// decide.MarkDisplay([]byte(`{"model":"d1","answers":{}}`)) | |
| 376 | +// // {"display":true,"model":"d1","answers":{}} | |
| 377 | +func MarkDisplay(raw []byte) []byte { | |
| 378 | + trimmed := bytes.TrimSpace(raw) | |
| 379 | + if len(trimmed) < 2 || trimmed[0] != '{' { | |
| 380 | + return raw | |
| 381 | + } | |
| 382 | + rest := bytes.TrimSpace(trimmed[1:]) | |
| 383 | + marked := []byte(`{"display":true`) | |
| 384 | + if rest[0] != '}' { | |
| 385 | + marked = append(marked, ',') | |
| 386 | + } | |
| 387 | + return append(marked, rest...) | |
| 388 | +} | |
| @@ -365,3 +365,24 @@ func likeliest(keys, labels []string, probabilities map[string]float64) string { | |||
| 365 | } | 365 | } |
| 366 | return labels[best] | 366 | return labels[best] |
| 367 | } | 367 | } |
| 368 | + | ||
| 369 | +// MarkDisplay returns the server's JSON object with `"display":true` added | ||
| 370 | +// at its head — what `-p` prints for a decider whose file sets | ||
| 371 | +// `decision.display: true`, so the hook that runs it knows the chat should | ||
| 372 | +// show the answer. Everything else is left byte for byte; a payload that is | ||
| 373 | +// not an object is returned unchanged. | ||
| 374 | +// | ||
| 375 | +// decide.MarkDisplay([]byte(`{"model":"d1","answers":{}}`)) | ||
| 376 | +// // {"display":true,"model":"d1","answers":{}} | ||
| 377 | +func MarkDisplay(raw []byte) []byte { | ||
| 378 | + trimmed := bytes.TrimSpace(raw) | ||
| 379 | + if len(trimmed) < 2 || trimmed[0] != '{' { | ||
| 380 | + return raw | ||
| 381 | + } | ||
| 382 | + rest := bytes.TrimSpace(trimmed[1:]) | ||
| 383 | + marked := []byte(`{"display":true`) | ||
| 384 | + if rest[0] != '}' { | ||
| 385 | + marked = append(marked, ',') | ||
| 386 | + } | ||
| 387 | + return append(marked, rest...) | ||
| 388 | +} | ||
modified
internal/decide/decide_test.go +15 -0 | @@ -212,3 +212,18 @@ func TestLinesSayWhenAQuestionWasNotAnswered(t *testing.T) { | ||
| 212 | 212 | t.Errorf("Lines = %q", got) |
| 213 | 213 | } |
| 214 | 214 | } |
| 215 | + | |
| 216 | +func TestMarkDisplayAddsTheFlagAtTheHeadOfTheObject(t *testing.T) { | |
| 217 | + cases := map[string]string{ | |
| 218 | + `{"model":"d1","answers":{}}`: `{"display":true,"model":"d1","answers":{}}`, | |
| 219 | + " {\n \"model\": \"d1\"}\n": `{"display":true,"model": "d1"}`, | |
| 220 | + `{}`: `{"display":true}`, | |
| 221 | + `[1,2]`: `[1,2]`, | |
| 222 | + ``: ``, | |
| 223 | + } | |
| 224 | + for raw, want := range cases { | |
| 225 | + if got := string(MarkDisplay([]byte(raw))); got != want { | |
| 226 | + t.Errorf("MarkDisplay(%q) = %q, want %q", raw, got, want) | |
| 227 | + } | |
| 228 | + } | |
| 229 | +} | |
| @@ -212,3 +212,18 @@ func TestLinesSayWhenAQuestionWasNotAnswered(t *testing.T) { | |||
| 212 | t.Errorf("Lines = %q", got) | 212 | t.Errorf("Lines = %q", got) |
| 213 | } | 213 | } |
| 214 | } | 214 | } |
| 215 | + | ||
| 216 | +func TestMarkDisplayAddsTheFlagAtTheHeadOfTheObject(t *testing.T) { | ||
| 217 | + cases := map[string]string{ | ||
| 218 | + `{"model":"d1","answers":{}}`: `{"display":true,"model":"d1","answers":{}}`, | ||
| 219 | + " {\n \"model\": \"d1\"}\n": `{"display":true,"model": "d1"}`, | ||
| 220 | + `{}`: `{"display":true}`, | ||
| 221 | + `[1,2]`: `[1,2]`, | ||
| 222 | + ``: ``, | ||
| 223 | + } | ||
| 224 | + for raw, want := range cases { | ||
| 225 | + if got := string(MarkDisplay([]byte(raw))); got != want { | ||
| 226 | + t.Errorf("MarkDisplay(%q) = %q, want %q", raw, got, want) | ||
| 227 | + } | ||
| 228 | + } | ||
| 229 | +} | ||
modified
internal/engine/hooks.go +5 -0 | @@ -43,6 +43,11 @@ func (e *Engine) generateWithHooks(ctx context.Context, messages []*ai.Message, | ||
| 43 | 43 | in := base |
| 44 | 44 | in.Prompt = question.Text() |
| 45 | 45 | d := e.hooks.PreGeneration(ctx, in) |
| 46 | + // Shown before anything else of the turn: what a hook (a decider's | |
| 47 | + // answer, say) wants the user to see is about the question. | |
| 48 | + for _, text := range d.Display { | |
| 49 | + ui.Display(text) | |
| 50 | + } | |
| 46 | 51 | |
| 47 | 52 | var ( |
| 48 | 53 | resp *ai.ModelResponse |
| @@ -43,6 +43,11 @@ func (e *Engine) generateWithHooks(ctx context.Context, messages []*ai.Message, | |||
| 43 | in := base | 43 | in := base |
| 44 | in.Prompt = question.Text() | 44 | in.Prompt = question.Text() |
| 45 | d := e.hooks.PreGeneration(ctx, in) | 45 | d := e.hooks.PreGeneration(ctx, in) |
| 46 | + // Shown before anything else of the turn: what a hook (a decider's | ||
| 47 | + // answer, say) wants the user to see is about the question. | ||
| 48 | + for _, text := range d.Display { | ||
| 49 | + ui.Display(text) | ||
| 50 | + } | ||
| 46 | 51 | ||
| 47 | var ( | 52 | var ( |
| 48 | resp *ai.ModelResponse | 53 | resp *ai.ModelResponse |
modified
internal/engine/hooks_test.go +38 -2 | @@ -46,10 +46,23 @@ type recordingSink struct { | ||
| 46 | 46 | tools []ui.ToolEvent |
| 47 | 47 | results []ui.ToolResult |
| 48 | 48 | notices []string |
| 49 | + shown []string | |
| 50 | + order []string // "display" and "text", in the order they arrived | |
| 49 | 51 | } |
| 50 | 52 | |
| 51 | -func (s *recordingSink) WorkDir() string { return s.dir } | |
| 52 | -func (s *recordingSink) Text(t string) { s.mu.Lock(); s.text.WriteString(t); s.mu.Unlock() } | |
| 53 | +func (s *recordingSink) WorkDir() string { return s.dir } | |
| 54 | +func (s *recordingSink) Text(t string) { | |
| 55 | + s.mu.Lock() | |
| 56 | + s.text.WriteString(t) | |
| 57 | + s.order = append(s.order, "text") | |
| 58 | + s.mu.Unlock() | |
| 59 | +} | |
| 60 | +func (s *recordingSink) Display(text string) { | |
| 61 | + s.mu.Lock() | |
| 62 | + s.shown = append(s.shown, text) | |
| 63 | + s.order = append(s.order, "display") | |
| 64 | + s.mu.Unlock() | |
| 65 | +} | |
| 53 | 66 | func (s *recordingSink) Reasoning(string) {} |
| 54 | 67 | func (s *recordingSink) ToolRunning(string) {} |
| 55 | 68 | func (s *recordingSink) ToolStart(ev ui.ToolEvent) { |
| @@ -113,6 +126,29 @@ func TestPreGenerationRewritesWhatTheModelReads(t *testing.T) { | ||
| 113 | 126 | } |
| 114 | 127 | } |
| 115 | 128 | |
| 129 | +// A hook's display reaches the front end before the answer streams, and | |
| 130 | +// never the model: it reads the context alone. | |
| 131 | +func TestPreGenerationDisplayIsShownBeforeTheAnswerAndNeverSent(t *testing.T) { | |
| 132 | + g, seen := scriptedGenkit(t, modelTurn(ai.NewTextPart("Sleep it is."))) | |
| 133 | + e := testEngine(g) | |
| 134 | + sink := hookedEngine(t, e, config.HooksConfig{PreGeneration: []config.Hook{ | |
| 135 | + {Command: `echo '{"context":"spell: sleep","display":"{ \\"spell\\": \\"sleep\\" }"}'`}, | |
| 136 | + }}) | |
| 137 | + | |
| 138 | + if _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("six kobolds")}, nil); err != nil { | |
| 139 | + t.Fatal(err) | |
| 140 | + } | |
| 141 | + if len(sink.shown) != 1 || sink.shown[0] != `{ "spell": "sleep" }` { | |
| 142 | + t.Errorf("shown = %q", sink.shown) | |
| 143 | + } | |
| 144 | + if len(sink.order) == 0 || sink.order[0] != "display" { | |
| 145 | + t.Errorf("order = %v, want the display first", sink.order) | |
| 146 | + } | |
| 147 | + if read := (*seen)[0].Messages[0].Text(); strings.Contains(read, `"spell"`) || !strings.Contains(read, "spell: sleep") { | |
| 148 | + t.Errorf("the model read %q", read) | |
| 149 | + } | |
| 150 | +} | |
| 151 | + | |
| 116 | 152 | // A stopped turn never reaches the model, shows the hook's response, and |
| 117 | 153 | // keeps a marker — never the blocked text — in the history. |
| 118 | 154 | func TestPreGenerationStopNeverCallsTheModel(t *testing.T) { |
| @@ -46,10 +46,23 @@ type recordingSink struct { | |||
| 46 | tools []ui.ToolEvent | 46 | tools []ui.ToolEvent |
| 47 | results []ui.ToolResult | 47 | results []ui.ToolResult |
| 48 | notices []string | 48 | notices []string |
| 49 | + shown []string | ||
| 50 | + order []string // "display" and "text", in the order they arrived | ||
| 49 | } | 51 | } |
| 50 | 52 | ||
| 51 | -func (s *recordingSink) WorkDir() string { return s.dir } | 53 | +func (s *recordingSink) WorkDir() string { return s.dir } |
| 52 | -func (s *recordingSink) Text(t string) { s.mu.Lock(); s.text.WriteString(t); s.mu.Unlock() } | 54 | +func (s *recordingSink) Text(t string) { |
| 55 | + s.mu.Lock() | ||
| 56 | + s.text.WriteString(t) | ||
| 57 | + s.order = append(s.order, "text") | ||
| 58 | + s.mu.Unlock() | ||
| 59 | +} | ||
| 60 | +func (s *recordingSink) Display(text string) { | ||
| 61 | + s.mu.Lock() | ||
| 62 | + s.shown = append(s.shown, text) | ||
| 63 | + s.order = append(s.order, "display") | ||
| 64 | + s.mu.Unlock() | ||
| 65 | +} | ||
| 53 | func (s *recordingSink) Reasoning(string) {} | 66 | func (s *recordingSink) Reasoning(string) {} |
| 54 | func (s *recordingSink) ToolRunning(string) {} | 67 | func (s *recordingSink) ToolRunning(string) {} |
| 55 | func (s *recordingSink) ToolStart(ev ui.ToolEvent) { | 68 | func (s *recordingSink) ToolStart(ev ui.ToolEvent) { |
| @@ -113,6 +126,29 @@ func TestPreGenerationRewritesWhatTheModelReads(t *testing.T) { | |||
| 113 | } | 126 | } |
| 114 | } | 127 | } |
| 115 | 128 | ||
| 129 | +// A hook's display reaches the front end before the answer streams, and | ||
| 130 | +// never the model: it reads the context alone. | ||
| 131 | +func TestPreGenerationDisplayIsShownBeforeTheAnswerAndNeverSent(t *testing.T) { | ||
| 132 | + g, seen := scriptedGenkit(t, modelTurn(ai.NewTextPart("Sleep it is."))) | ||
| 133 | + e := testEngine(g) | ||
| 134 | + sink := hookedEngine(t, e, config.HooksConfig{PreGeneration: []config.Hook{ | ||
| 135 | + {Command: `echo '{"context":"spell: sleep","display":"{ \\"spell\\": \\"sleep\\" }"}'`}, | ||
| 136 | + }}) | ||
| 137 | + | ||
| 138 | + if _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("six kobolds")}, nil); err != nil { | ||
| 139 | + t.Fatal(err) | ||
| 140 | + } | ||
| 141 | + if len(sink.shown) != 1 || sink.shown[0] != `{ "spell": "sleep" }` { | ||
| 142 | + t.Errorf("shown = %q", sink.shown) | ||
| 143 | + } | ||
| 144 | + if len(sink.order) == 0 || sink.order[0] != "display" { | ||
| 145 | + t.Errorf("order = %v, want the display first", sink.order) | ||
| 146 | + } | ||
| 147 | + if read := (*seen)[0].Messages[0].Text(); strings.Contains(read, `"spell"`) || !strings.Contains(read, "spell: sleep") { | ||
| 148 | + t.Errorf("the model read %q", read) | ||
| 149 | + } | ||
| 150 | +} | ||
| 151 | + | ||
| 116 | // A stopped turn never reaches the model, shows the hook's response, and | 152 | // A stopped turn never reaches the model, shows the hook's response, and |
| 117 | // keeps a marker — never the blocked text — in the history. | 153 | // keeps a marker — never the blocked text — in the history. |
| 118 | func TestPreGenerationStopNeverCallsTheModel(t *testing.T) { | 154 | func TestPreGenerationStopNeverCallsTheModel(t *testing.T) { |
modified
internal/hooks/display.go +3 -0 | @@ -84,6 +84,9 @@ func decision(event string, reply Reply, err error) string { | ||
| 84 | 84 | if c := strings.TrimSpace(reply.Context); c != "" && readsContext(event) { |
| 85 | 85 | changes = append(changes, fmt.Sprintf("context (%d chars)", utf8.RuneCountInString(c))) |
| 86 | 86 | } |
| 87 | + if s := strings.TrimSpace(reply.Display); s != "" && event == config.HookPreGeneration { | |
| 88 | + changes = append(changes, fmt.Sprintf("display (%d chars)", utf8.RuneCountInString(s))) | |
| 89 | + } | |
| 87 | 90 | if len(changes) == 0 { |
| 88 | 91 | return DecisionContinue |
| 89 | 92 | } |
| @@ -84,6 +84,9 @@ func decision(event string, reply Reply, err error) string { | |||
| 84 | if c := strings.TrimSpace(reply.Context); c != "" && readsContext(event) { | 84 | if c := strings.TrimSpace(reply.Context); c != "" && readsContext(event) { |
| 85 | changes = append(changes, fmt.Sprintf("context (%d chars)", utf8.RuneCountInString(c))) | 85 | changes = append(changes, fmt.Sprintf("context (%d chars)", utf8.RuneCountInString(c))) |
| 86 | } | 86 | } |
| 87 | + if s := strings.TrimSpace(reply.Display); s != "" && event == config.HookPreGeneration { | ||
| 88 | + changes = append(changes, fmt.Sprintf("display (%d chars)", utf8.RuneCountInString(s))) | ||
| 89 | + } | ||
| 87 | if len(changes) == 0 { | 90 | if len(changes) == 0 { |
| 88 | return DecisionContinue | 91 | return DecisionContinue |
| 89 | } | 92 | } |
modified
internal/hooks/display_test.go +2 -0 | @@ -40,6 +40,8 @@ func TestDecisionCountsOnlyWhatTheEventReads(t *testing.T) { | ||
| 40 | 40 | {"allow and input", config.HookPreToolCall, Reply{Action: ActionAllow, Input: json.RawMessage(`{}`)}, nil, "allow + input replaced"}, |
| 41 | 41 | {"input", config.HookPreToolCall, Reply{Input: json.RawMessage(`{"command":"ls"}`)}, nil, "input replaced"}, |
| 42 | 42 | {"tool output context", config.HookPostToolCall, Reply{Context: "ok"}, nil, "context (2 chars)"}, |
| 43 | + {"context and display", config.HookPreGeneration, Reply{Context: "abc", Display: " {} "}, nil, "context (3 chars) + display (2 chars)"}, | |
| 44 | + {"display ignored outside preGeneration", config.HookPostToolCall, Reply{Display: "x"}, nil, DecisionContinue}, | |
| 43 | 45 | {"ask rewrites", config.HookPreAsk, Reply{Prompt: ptr("q"), Transcript: ptr("")}, nil, "prompt replaced + transcript replaced"}, |
| 44 | 46 | {"answer", config.HookPostAsk, Reply{Answer: ptr("a")}, nil, "answer replaced"}, |
| 45 | 47 | {"postGeneration only observes", config.HookPostGeneration, Reply{Context: "ignored", Prompt: ptr("ignored")}, nil, DecisionContinue}, |
| @@ -40,6 +40,8 @@ func TestDecisionCountsOnlyWhatTheEventReads(t *testing.T) { | |||
| 40 | {"allow and input", config.HookPreToolCall, Reply{Action: ActionAllow, Input: json.RawMessage(`{}`)}, nil, "allow + input replaced"}, | 40 | {"allow and input", config.HookPreToolCall, Reply{Action: ActionAllow, Input: json.RawMessage(`{}`)}, nil, "allow + input replaced"}, |
| 41 | {"input", config.HookPreToolCall, Reply{Input: json.RawMessage(`{"command":"ls"}`)}, nil, "input replaced"}, | 41 | {"input", config.HookPreToolCall, Reply{Input: json.RawMessage(`{"command":"ls"}`)}, nil, "input replaced"}, |
| 42 | {"tool output context", config.HookPostToolCall, Reply{Context: "ok"}, nil, "context (2 chars)"}, | 42 | {"tool output context", config.HookPostToolCall, Reply{Context: "ok"}, nil, "context (2 chars)"}, |
| 43 | + {"context and display", config.HookPreGeneration, Reply{Context: "abc", Display: " {} "}, nil, "context (3 chars) + display (2 chars)"}, | ||
| 44 | + {"display ignored outside preGeneration", config.HookPostToolCall, Reply{Display: "x"}, nil, DecisionContinue}, | ||
| 43 | {"ask rewrites", config.HookPreAsk, Reply{Prompt: ptr("q"), Transcript: ptr("")}, nil, "prompt replaced + transcript replaced"}, | 45 | {"ask rewrites", config.HookPreAsk, Reply{Prompt: ptr("q"), Transcript: ptr("")}, nil, "prompt replaced + transcript replaced"}, |
| 44 | {"answer", config.HookPostAsk, Reply{Answer: ptr("a")}, nil, "answer replaced"}, | 46 | {"answer", config.HookPostAsk, Reply{Answer: ptr("a")}, nil, "answer replaced"}, |
| 45 | {"postGeneration only observes", config.HookPostGeneration, Reply{Context: "ignored", Prompt: ptr("ignored")}, nil, DecisionContinue}, | 47 | {"postGeneration only observes", config.HookPostGeneration, Reply{Context: "ignored", Prompt: ptr("ignored")}, nil, DecisionContinue}, |
modified
internal/hooks/hooks.go +11 -0 | @@ -100,6 +100,11 @@ type Reply struct { | ||
| 100 | 100 | // (preGeneration), after the tool's output (postToolCall), after the |
| 101 | 101 | // question sent to another agent (preAsk), after its answer (postAsk). |
| 102 | 102 | Context string `json:"context,omitempty"` |
| 103 | + // Display is text shown to the user, never to a model (preGeneration): | |
| 104 | + // in its own colour in the window and the terminal, as a fenced block in | |
| 105 | + // an editor, before the answer. A hook that asked a decider hands back | |
| 106 | + // its answer here. | |
| 107 | + Display string `json:"display,omitempty"` | |
| 103 | 108 | // Input replaces the tool call's arguments (preToolCall). Absent = kept. |
| 104 | 109 | Input json.RawMessage `json:"input,omitempty"` |
| 105 | 110 | // Reason says why a turn was stopped or a call denied. |
| @@ -139,6 +144,9 @@ type PromptDecision struct { | ||
| 139 | 144 | Prompt string |
| 140 | 145 | // Context holds the text the hooks added, in hook order. |
| 141 | 146 | Context []string |
| 147 | + // Display holds the text the hooks asked to show the user, in hook | |
| 148 | + // order — kept even when a later hook stops the turn. | |
| 149 | + Display []string | |
| 142 | 150 | // Stopped means the model must not be called; Response is what the user |
| 143 | 151 | // reads instead and Reason why. |
| 144 | 152 | Stopped bool |
| @@ -168,6 +176,9 @@ func (r *Runner) PreGeneration(ctx context.Context, in Input) PromptDecision { | ||
| 168 | 176 | } |
| 169 | 177 | continue |
| 170 | 178 | } |
| 179 | + if s := strings.TrimSpace(reply.Display); s != "" { | |
| 180 | + d.Display = append(d.Display, s) | |
| 181 | + } | |
| 171 | 182 | if reply.Action == ActionStop { |
| 172 | 183 | return stopped(d, reply.Reason, reply.Response) |
| 173 | 184 | } |
| @@ -100,6 +100,11 @@ type Reply struct { | |||
| 100 | // (preGeneration), after the tool's output (postToolCall), after the | 100 | // (preGeneration), after the tool's output (postToolCall), after the |
| 101 | // question sent to another agent (preAsk), after its answer (postAsk). | 101 | // question sent to another agent (preAsk), after its answer (postAsk). |
| 102 | Context string `json:"context,omitempty"` | 102 | Context string `json:"context,omitempty"` |
| 103 | + // Display is text shown to the user, never to a model (preGeneration): | ||
| 104 | + // in its own colour in the window and the terminal, as a fenced block in | ||
| 105 | + // an editor, before the answer. A hook that asked a decider hands back | ||
| 106 | + // its answer here. | ||
| 107 | + Display string `json:"display,omitempty"` | ||
| 103 | // Input replaces the tool call's arguments (preToolCall). Absent = kept. | 108 | // Input replaces the tool call's arguments (preToolCall). Absent = kept. |
| 104 | Input json.RawMessage `json:"input,omitempty"` | 109 | Input json.RawMessage `json:"input,omitempty"` |
| 105 | // Reason says why a turn was stopped or a call denied. | 110 | // Reason says why a turn was stopped or a call denied. |
| @@ -139,6 +144,9 @@ type PromptDecision struct { | |||
| 139 | Prompt string | 144 | Prompt string |
| 140 | // Context holds the text the hooks added, in hook order. | 145 | // Context holds the text the hooks added, in hook order. |
| 141 | Context []string | 146 | Context []string |
| 147 | + // Display holds the text the hooks asked to show the user, in hook | ||
| 148 | + // order — kept even when a later hook stops the turn. | ||
| 149 | + Display []string | ||
| 142 | // Stopped means the model must not be called; Response is what the user | 150 | // Stopped means the model must not be called; Response is what the user |
| 143 | // reads instead and Reason why. | 151 | // reads instead and Reason why. |
| 144 | Stopped bool | 152 | Stopped bool |
| @@ -168,6 +176,9 @@ func (r *Runner) PreGeneration(ctx context.Context, in Input) PromptDecision { | |||
| 168 | } | 176 | } |
| 169 | continue | 177 | continue |
| 170 | } | 178 | } |
| 179 | + if s := strings.TrimSpace(reply.Display); s != "" { | ||
| 180 | + d.Display = append(d.Display, s) | ||
| 181 | + } | ||
| 171 | if reply.Action == ActionStop { | 182 | if reply.Action == ActionStop { |
| 172 | return stopped(d, reply.Reason, reply.Response) | 183 | return stopped(d, reply.Reason, reply.Response) |
| 173 | } | 184 | } |
modified
internal/hooks/hooks_test.go +18 -0 | @@ -121,6 +121,24 @@ func TestPreGenerationStopEndsTheChain(t *testing.T) { | ||
| 121 | 121 | } |
| 122 | 122 | } |
| 123 | 123 | |
| 124 | +// What hooks ask to show the user accumulates in order, apart from the | |
| 125 | +// context the model reads — and survives a later stop. | |
| 126 | +func TestPreGenerationCollectsDisplayInOrder(t *testing.T) { | |
| 127 | + cfg := config.HooksConfig{PreGeneration: []config.Hook{ | |
| 128 | + hook(`echo '{"context":"spell: sleep","display":"{\\"spell\\": \\"sleep\\"}"}'`), | |
| 129 | + hook(`echo '{"display":" second "}'`), | |
| 130 | + hook(`echo '{"action":"stop","reason":"enough","display":"third"}'`), | |
| 131 | + }} | |
| 132 | + r, _ := runner(cfg) | |
| 133 | + d := r.PreGeneration(context.Background(), Input{Prompt: "kobolds"}) | |
| 134 | + if strings.Join(d.Display, "|") != `{"spell": "sleep"}|second|third` { | |
| 135 | + t.Errorf("Display = %q", d.Display) | |
| 136 | + } | |
| 137 | + if strings.Join(d.Context, "|") != "spell: sleep" || !d.Stopped { | |
| 138 | + t.Errorf("decision = %+v", d) | |
| 139 | + } | |
| 140 | +} | |
| 141 | + | |
| 124 | 142 | // A stop without a response still tells the user something. |
| 125 | 143 | func TestStopWithoutResponseExplainsTheReason(t *testing.T) { |
| 126 | 144 | r, _ := runner(config.HooksConfig{PreGeneration: []config.Hook{hook(`echo '{"action":"stop","reason":"too short"}'`)}}) |
| @@ -121,6 +121,24 @@ func TestPreGenerationStopEndsTheChain(t *testing.T) { | |||
| 121 | } | 121 | } |
| 122 | } | 122 | } |
| 123 | 123 | ||
| 124 | +// What hooks ask to show the user accumulates in order, apart from the | ||
| 125 | +// context the model reads — and survives a later stop. | ||
| 126 | +func TestPreGenerationCollectsDisplayInOrder(t *testing.T) { | ||
| 127 | + cfg := config.HooksConfig{PreGeneration: []config.Hook{ | ||
| 128 | + hook(`echo '{"context":"spell: sleep","display":"{\\"spell\\": \\"sleep\\"}"}'`), | ||
| 129 | + hook(`echo '{"display":" second "}'`), | ||
| 130 | + hook(`echo '{"action":"stop","reason":"enough","display":"third"}'`), | ||
| 131 | + }} | ||
| 132 | + r, _ := runner(cfg) | ||
| 133 | + d := r.PreGeneration(context.Background(), Input{Prompt: "kobolds"}) | ||
| 134 | + if strings.Join(d.Display, "|") != `{"spell": "sleep"}|second|third` { | ||
| 135 | + t.Errorf("Display = %q", d.Display) | ||
| 136 | + } | ||
| 137 | + if strings.Join(d.Context, "|") != "spell: sleep" || !d.Stopped { | ||
| 138 | + t.Errorf("decision = %+v", d) | ||
| 139 | + } | ||
| 140 | +} | ||
| 141 | + | ||
| 124 | // A stop without a response still tells the user something. | 142 | // A stop without a response still tells the user something. |
| 125 | func TestStopWithoutResponseExplainsTheReason(t *testing.T) { | 143 | func TestStopWithoutResponseExplainsTheReason(t *testing.T) { |
| 126 | r, _ := runner(config.HooksConfig{PreGeneration: []config.Hook{hook(`echo '{"action":"stop","reason":"too short"}'`)}}) | 144 | r, _ := runner(config.HooksConfig{PreGeneration: []config.Hook{hook(`echo '{"action":"stop","reason":"too short"}'`)}}) |
modified
internal/tui/render.go +14 -0 | @@ -22,6 +22,7 @@ const ( | ||
| 22 | 22 | styleDiffAdd |
| 23 | 23 | styleDiffDel |
| 24 | 24 | styleThinking |
| 25 | + styleDisplay | |
| 25 | 26 | ) |
| 26 | 27 | |
| 27 | 28 | // Line is one drawn line of the conversation: its text, wrapped to the width |
| @@ -69,6 +70,8 @@ func linesOf(e Entry, agentName string, width, previewLines int) []Line { | ||
| 69 | 70 | lines = toolLines(e, width, previewLines) |
| 70 | 71 | case KindThinking: |
| 71 | 72 | lines = append(label("thinking"), prose(e.Text, width, styleThinking)...) |
| 73 | + case KindDisplay: | |
| 74 | + lines = append(label("hook"), verbatim(e.Text, styleDisplay)...) | |
| 72 | 75 | case KindError: |
| 73 | 76 | lines = prose(e.Text, width, styleError) |
| 74 | 77 | default: |
| @@ -91,6 +94,17 @@ func prose(text string, width int, style styleKind) []Line { | ||
| 91 | 94 | return out |
| 92 | 95 | } |
| 93 | 96 | |
| 97 | +// verbatim lays text out line for line, indented and never wrapped — a | |
| 98 | +// hook's display is often JSON, whose indentation is its meaning; the | |
| 99 | +// screen clips what is wider than the window, as it does fenced code. | |
| 100 | +func verbatim(text string, style styleKind) []Line { | |
| 101 | + var out []Line | |
| 102 | + for _, line := range strings.Split(text, "\n") { | |
| 103 | + out = append(out, Line{Text: pad + line, Style: style}) | |
| 104 | + } | |
| 105 | + return out | |
| 106 | +} | |
| 107 | + | |
| 94 | 108 | // body lays out an answer: prose wrapped, fenced blocks drawn as code. |
| 95 | 109 | func body(text string, width int) []Line { |
| 96 | 110 | var out []Line |
| @@ -22,6 +22,7 @@ const ( | |||
| 22 | styleDiffAdd | 22 | styleDiffAdd |
| 23 | styleDiffDel | 23 | styleDiffDel |
| 24 | styleThinking | 24 | styleThinking |
| 25 | + styleDisplay | ||
| 25 | ) | 26 | ) |
| 26 | 27 | ||
| 27 | // Line is one drawn line of the conversation: its text, wrapped to the width | 28 | // Line is one drawn line of the conversation: its text, wrapped to the width |
| @@ -69,6 +70,8 @@ func linesOf(e Entry, agentName string, width, previewLines int) []Line { | |||
| 69 | lines = toolLines(e, width, previewLines) | 70 | lines = toolLines(e, width, previewLines) |
| 70 | case KindThinking: | 71 | case KindThinking: |
| 71 | lines = append(label("thinking"), prose(e.Text, width, styleThinking)...) | 72 | lines = append(label("thinking"), prose(e.Text, width, styleThinking)...) |
| 73 | + case KindDisplay: | ||
| 74 | + lines = append(label("hook"), verbatim(e.Text, styleDisplay)...) | ||
| 72 | case KindError: | 75 | case KindError: |
| 73 | lines = prose(e.Text, width, styleError) | 76 | lines = prose(e.Text, width, styleError) |
| 74 | default: | 77 | default: |
| @@ -91,6 +94,17 @@ func prose(text string, width int, style styleKind) []Line { | |||
| 91 | return out | 94 | return out |
| 92 | } | 95 | } |
| 93 | 96 | ||
| 97 | +// verbatim lays text out line for line, indented and never wrapped — a | ||
| 98 | +// hook's display is often JSON, whose indentation is its meaning; the | ||
| 99 | +// screen clips what is wider than the window, as it does fenced code. | ||
| 100 | +func verbatim(text string, style styleKind) []Line { | ||
| 101 | + var out []Line | ||
| 102 | + for _, line := range strings.Split(text, "\n") { | ||
| 103 | + out = append(out, Line{Text: pad + line, Style: style}) | ||
| 104 | + } | ||
| 105 | + return out | ||
| 106 | +} | ||
| 107 | + | ||
| 94 | // body lays out an answer: prose wrapped, fenced blocks drawn as code. | 108 | // body lays out an answer: prose wrapped, fenced blocks drawn as code. |
| 95 | func body(text string, width int) []Line { | 109 | func body(text string, width int) []Line { |
| 96 | var out []Line | 110 | var out []Line |
modified
internal/tui/sink.go +7 -0 | @@ -92,6 +92,13 @@ func (k *sink) ToolEnd(id string, res ui.ToolResult) { | ||
| 92 | 92 | k.r.wake() |
| 93 | 93 | } |
| 94 | 94 | |
| 95 | +// Display shows what a hook asked to show the user (a decider's answer) as | |
| 96 | +// its own entry, in its own colour, before the answer streams. | |
| 97 | +func (k *sink) Display(text string) { | |
| 98 | + k.r.t.Display(text) | |
| 99 | + k.r.wake() | |
| 100 | +} | |
| 101 | + | |
| 95 | 102 | // Notice shows a remark about the turn (a hook that failed, say) as a dim |
| 96 | 103 | // line of the transcript: printed on stdout, it would tear the screen. |
| 97 | 104 | func (k *sink) Notice(text string) { |
| @@ -92,6 +92,13 @@ func (k *sink) ToolEnd(id string, res ui.ToolResult) { | |||
| 92 | k.r.wake() | 92 | k.r.wake() |
| 93 | } | 93 | } |
| 94 | 94 | ||
| 95 | +// Display shows what a hook asked to show the user (a decider's answer) as | ||
| 96 | +// its own entry, in its own colour, before the answer streams. | ||
| 97 | +func (k *sink) Display(text string) { | ||
| 98 | + k.r.t.Display(text) | ||
| 99 | + k.r.wake() | ||
| 100 | +} | ||
| 101 | + | ||
| 95 | // Notice shows a remark about the turn (a hook that failed, say) as a dim | 102 | // Notice shows a remark about the turn (a hook that failed, say) as a dim |
| 96 | // line of the transcript: printed on stdout, it would tear the screen. | 103 | // line of the transcript: printed on stdout, it would tear the screen. |
| 97 | func (k *sink) Notice(text string) { | 104 | func (k *sink) Notice(text string) { |
modified
internal/tui/style.go +2 -0 | @@ -24,6 +24,7 @@ var ( | ||
| 24 | 24 | stAsk = tcell.StyleDefault.Bold(true).Foreground(tcell.ColorYellow) |
| 25 | 25 | stPaste = tcell.StyleDefault.Reverse(true).Foreground(tcell.ColorTeal) |
| 26 | 26 | stThinking = tcell.StyleDefault.Dim(true).Italic(true).Foreground(tcell.ColorPurple) |
| 27 | + stDisplay = tcell.StyleDefault.Foreground(tcell.ColorBlue) | |
| 27 | 28 | stScroll = tcell.StyleDefault.Dim(true) |
| 28 | 29 | stThumb = tcell.StyleDefault.Reverse(true) |
| 29 | 30 | ) |
| @@ -41,6 +42,7 @@ var styles = map[styleKind]tcell.Style{ | ||
| 41 | 42 | styleDiffAdd: stDiffAdd, |
| 42 | 43 | styleDiffDel: stDiffDel, |
| 43 | 44 | styleThinking: stThinking, |
| 45 | + styleDisplay: stDisplay, | |
| 44 | 46 | } |
| 45 | 47 | |
| 46 | 48 | // styleOf returns the screen style of a laid-out line's kind. |
| @@ -24,6 +24,7 @@ var ( | |||
| 24 | stAsk = tcell.StyleDefault.Bold(true).Foreground(tcell.ColorYellow) | 24 | stAsk = tcell.StyleDefault.Bold(true).Foreground(tcell.ColorYellow) |
| 25 | stPaste = tcell.StyleDefault.Reverse(true).Foreground(tcell.ColorTeal) | 25 | stPaste = tcell.StyleDefault.Reverse(true).Foreground(tcell.ColorTeal) |
| 26 | stThinking = tcell.StyleDefault.Dim(true).Italic(true).Foreground(tcell.ColorPurple) | 26 | stThinking = tcell.StyleDefault.Dim(true).Italic(true).Foreground(tcell.ColorPurple) |
| 27 | + stDisplay = tcell.StyleDefault.Foreground(tcell.ColorBlue) | ||
| 27 | stScroll = tcell.StyleDefault.Dim(true) | 28 | stScroll = tcell.StyleDefault.Dim(true) |
| 28 | stThumb = tcell.StyleDefault.Reverse(true) | 29 | stThumb = tcell.StyleDefault.Reverse(true) |
| 29 | ) | 30 | ) |
| @@ -41,6 +42,7 @@ var styles = map[styleKind]tcell.Style{ | |||
| 41 | styleDiffAdd: stDiffAdd, | 42 | styleDiffAdd: stDiffAdd, |
| 42 | styleDiffDel: stDiffDel, | 43 | styleDiffDel: stDiffDel, |
| 43 | styleThinking: stThinking, | 44 | styleThinking: stThinking, |
| 45 | + styleDisplay: stDisplay, | ||
| 44 | } | 46 | } |
| 45 | 47 | ||
| 46 | // styleOf returns the screen style of a laid-out line's kind. | 48 | // styleOf returns the screen style of a laid-out line's kind. |
modified
internal/tui/transcript.go +6 -0 | @@ -27,6 +27,9 @@ const ( | ||
| 27 | 27 | // KindThinking is what a thinking model streamed before its answer, |
| 28 | 28 | // chunk by chunk into ONE entry, as KindAgent is. |
| 29 | 29 | KindThinking |
| 30 | + // KindDisplay is text a preGeneration hook asked to show the user — a | |
| 31 | + // decider's answer, say: never the answer, never sent to the model. | |
| 32 | + KindDisplay | |
| 30 | 33 | ) |
| 31 | 34 | |
| 32 | 35 | // Tool statuses, as the transcript shows them. |
| @@ -124,6 +127,9 @@ func (t *Transcript) AddUserFull(display, full string) { | ||
| 124 | 127 | // Notice appends a line the window says for itself. |
| 125 | 128 | func (t *Transcript) Notice(text string) { t.add(Entry{Kind: KindNotice, Text: text}) } |
| 126 | 129 | |
| 130 | +// Display appends what a hook asked to show the user. | |
| 131 | +func (t *Transcript) Display(text string) { t.add(Entry{Kind: KindDisplay, Text: text}) } | |
| 132 | + | |
| 127 | 133 | // Error appends a one-line failure. |
| 128 | 134 | func (t *Transcript) Error(text string) { t.add(Entry{Kind: KindError, Text: text}) } |
| 129 | 135 | |
| @@ -27,6 +27,9 @@ const ( | |||
| 27 | // KindThinking is what a thinking model streamed before its answer, | 27 | // KindThinking is what a thinking model streamed before its answer, |
| 28 | // chunk by chunk into ONE entry, as KindAgent is. | 28 | // chunk by chunk into ONE entry, as KindAgent is. |
| 29 | KindThinking | 29 | KindThinking |
| 30 | + // KindDisplay is text a preGeneration hook asked to show the user — a | ||
| 31 | + // decider's answer, say: never the answer, never sent to the model. | ||
| 32 | + KindDisplay | ||
| 30 | ) | 33 | ) |
| 31 | 34 | ||
| 32 | // Tool statuses, as the transcript shows them. | 35 | // Tool statuses, as the transcript shows them. |
| @@ -124,6 +127,9 @@ func (t *Transcript) AddUserFull(display, full string) { | |||
| 124 | // Notice appends a line the window says for itself. | 127 | // Notice appends a line the window says for itself. |
| 125 | func (t *Transcript) Notice(text string) { t.add(Entry{Kind: KindNotice, Text: text}) } | 128 | func (t *Transcript) Notice(text string) { t.add(Entry{Kind: KindNotice, Text: text}) } |
| 126 | 129 | ||
| 130 | +// Display appends what a hook asked to show the user. | ||
| 131 | +func (t *Transcript) Display(text string) { t.add(Entry{Kind: KindDisplay, Text: text}) } | ||
| 132 | + | ||
| 127 | // Error appends a one-line failure. | 133 | // Error appends a one-line failure. |
| 128 | func (t *Transcript) Error(text string) { t.add(Entry{Kind: KindError, Text: text}) } | 134 | func (t *Transcript) Error(text string) { t.add(Entry{Kind: KindError, Text: text}) } |
| 129 | 135 | ||
modified
internal/tui/transcript_test.go +26 -0 | @@ -170,3 +170,29 @@ func TestAnswersAndPlainAreRawText(t *testing.T) { | ||
| 170 | 170 | t.Errorf("Plain:\n got %q\nwant %q", got, want) |
| 171 | 171 | } |
| 172 | 172 | } |
| 173 | + | |
| 174 | +// A hook's display is its own entry, labelled, drawn line for line in its | |
| 175 | +// own colour: JSON keeps its indentation, nothing is wrapped. | |
| 176 | +func TestDisplayIsDrawnVerbatimUnderItsLabel(t *testing.T) { | |
| 177 | + tr := &Transcript{} | |
| 178 | + tr.Display("{\n \"spell\": \"a very long spell name that is wider than the window\"\n}") | |
| 179 | + entries := tr.Entries() | |
| 180 | + if len(entries) != 1 || entries[0].Kind != KindDisplay { | |
| 181 | + t.Fatalf("entries = %+v", entries) | |
| 182 | + } | |
| 183 | + lines := layout(entries, "mm", 20, 0) | |
| 184 | + want := []Line{ | |
| 185 | + {Text: "‣ hook", Style: styleLabel}, | |
| 186 | + {Text: " {", Style: styleDisplay}, | |
| 187 | + {Text: ` "spell": "a very long spell name that is wider than the window"`, Style: styleDisplay}, | |
| 188 | + {Text: " }", Style: styleDisplay}, | |
| 189 | + } | |
| 190 | + if len(lines) != len(want) { | |
| 191 | + t.Fatalf("lines = %+v", lines) | |
| 192 | + } | |
| 193 | + for i := range want { | |
| 194 | + if lines[i] != want[i] { | |
| 195 | + t.Errorf("line %d = %+v, want %+v", i, lines[i], want[i]) | |
| 196 | + } | |
| 197 | + } | |
| 198 | +} | |
| @@ -170,3 +170,29 @@ func TestAnswersAndPlainAreRawText(t *testing.T) { | |||
| 170 | t.Errorf("Plain:\n got %q\nwant %q", got, want) | 170 | t.Errorf("Plain:\n got %q\nwant %q", got, want) |
| 171 | } | 171 | } |
| 172 | } | 172 | } |
| 173 | + | ||
| 174 | +// A hook's display is its own entry, labelled, drawn line for line in its | ||
| 175 | +// own colour: JSON keeps its indentation, nothing is wrapped. | ||
| 176 | +func TestDisplayIsDrawnVerbatimUnderItsLabel(t *testing.T) { | ||
| 177 | + tr := &Transcript{} | ||
| 178 | + tr.Display("{\n \"spell\": \"a very long spell name that is wider than the window\"\n}") | ||
| 179 | + entries := tr.Entries() | ||
| 180 | + if len(entries) != 1 || entries[0].Kind != KindDisplay { | ||
| 181 | + t.Fatalf("entries = %+v", entries) | ||
| 182 | + } | ||
| 183 | + lines := layout(entries, "mm", 20, 0) | ||
| 184 | + want := []Line{ | ||
| 185 | + {Text: "‣ hook", Style: styleLabel}, | ||
| 186 | + {Text: " {", Style: styleDisplay}, | ||
| 187 | + {Text: ` "spell": "a very long spell name that is wider than the window"`, Style: styleDisplay}, | ||
| 188 | + {Text: " }", Style: styleDisplay}, | ||
| 189 | + } | ||
| 190 | + if len(lines) != len(want) { | ||
| 191 | + t.Fatalf("lines = %+v", lines) | ||
| 192 | + } | ||
| 193 | + for i := range want { | ||
| 194 | + if lines[i] != want[i] { | ||
| 195 | + t.Errorf("line %d = %+v, want %+v", i, lines[i], want[i]) | ||
| 196 | + } | ||
| 197 | + } | ||
| 198 | +} | ||
modified
internal/ui/ui.go +29 -0 | @@ -25,6 +25,7 @@ import ( | ||
| 25 | 25 | "sync/atomic" |
| 26 | 26 | |
| 27 | 27 | "mm/internal/elicit" |
| 28 | + "mm/internal/spinner" | |
| 28 | 29 | ) |
| 29 | 30 | |
| 30 | 31 | // Out is where human-facing text goes when no Sink is active: the banner, the |
| @@ -155,6 +156,34 @@ func Notice(text string) { | ||
| 155 | 156 | _, _ = fmt.Fprintln(Out, text) |
| 156 | 157 | } |
| 157 | 158 | |
| 159 | +// Displayer is the optional side of a Sink that shows text a hook asked to | |
| 160 | +// show the user (a preGeneration reply's `display`): never the answer, never | |
| 161 | +// sent to a model, drawn apart from both — its own colour in the window, a | |
| 162 | +// fenced block in an editor. | |
| 163 | +type Displayer interface { | |
| 164 | + Display(text string) | |
| 165 | +} | |
| 166 | + | |
| 167 | +// displayColour is the terminal's colour for Display: bright blue, apart | |
| 168 | +// from the answer (plain), the notices (dim) and the ⚙ recap (green). | |
| 169 | +const displayColour = "\033[94m" | |
| 170 | + | |
| 171 | +// Display shows text a hook asked to show the user, before the answer: the | |
| 172 | +// active sink's Displayer, or the text on Out — coloured on a terminal, | |
| 173 | +// plain in a pipe. | |
| 174 | +// | |
| 175 | +// ui.Display("{\n \"spell\": \"sleep\"\n}") | |
| 176 | +func Display(text string) { | |
| 177 | + if d, ok := ActiveSink().(Displayer); ok { | |
| 178 | + d.Display(text) | |
| 179 | + return | |
| 180 | + } | |
| 181 | + if spinner.Styled() { | |
| 182 | + text = displayColour + text + "\033[0m" | |
| 183 | + } | |
| 184 | + _, _ = fmt.Fprintln(Out, text) | |
| 185 | +} | |
| 186 | + | |
| 158 | 187 | // Speaker is the optional side of a Sink that shows WHICH agent is answering: |
| 159 | 188 | // between Speaking and Spoken, what streams through the sink (text, tool |
| 160 | 189 | // calls) comes from another agent — the one the ask_agent tool asked — and |
| @@ -25,6 +25,7 @@ import ( | |||
| 25 | "sync/atomic" | 25 | "sync/atomic" |
| 26 | 26 | ||
| 27 | "mm/internal/elicit" | 27 | "mm/internal/elicit" |
| 28 | + "mm/internal/spinner" | ||
| 28 | ) | 29 | ) |
| 29 | 30 | ||
| 30 | // Out is where human-facing text goes when no Sink is active: the banner, the | 31 | // Out is where human-facing text goes when no Sink is active: the banner, the |
| @@ -155,6 +156,34 @@ func Notice(text string) { | |||
| 155 | _, _ = fmt.Fprintln(Out, text) | 156 | _, _ = fmt.Fprintln(Out, text) |
| 156 | } | 157 | } |
| 157 | 158 | ||
| 159 | +// Displayer is the optional side of a Sink that shows text a hook asked to | ||
| 160 | +// show the user (a preGeneration reply's `display`): never the answer, never | ||
| 161 | +// sent to a model, drawn apart from both — its own colour in the window, a | ||
| 162 | +// fenced block in an editor. | ||
| 163 | +type Displayer interface { | ||
| 164 | + Display(text string) | ||
| 165 | +} | ||
| 166 | + | ||
| 167 | +// displayColour is the terminal's colour for Display: bright blue, apart | ||
| 168 | +// from the answer (plain), the notices (dim) and the ⚙ recap (green). | ||
| 169 | +const displayColour = "\033[94m" | ||
| 170 | + | ||
| 171 | +// Display shows text a hook asked to show the user, before the answer: the | ||
| 172 | +// active sink's Displayer, or the text on Out — coloured on a terminal, | ||
| 173 | +// plain in a pipe. | ||
| 174 | +// | ||
| 175 | +// ui.Display("{\n \"spell\": \"sleep\"\n}") | ||
| 176 | +func Display(text string) { | ||
| 177 | + if d, ok := ActiveSink().(Displayer); ok { | ||
| 178 | + d.Display(text) | ||
| 179 | + return | ||
| 180 | + } | ||
| 181 | + if spinner.Styled() { | ||
| 182 | + text = displayColour + text + "\033[0m" | ||
| 183 | + } | ||
| 184 | + _, _ = fmt.Fprintln(Out, text) | ||
| 185 | +} | ||
| 186 | + | ||
| 158 | // Speaker is the optional side of a Sink that shows WHICH agent is answering: | 187 | // Speaker is the optional side of a Sink that shows WHICH agent is answering: |
| 159 | // between Speaking and Spoken, what streams through the sink (text, tool | 188 | // between Speaking and Spoken, what streams through the sink (text, tool |
| 160 | // calls) comes from another agent — the one the ask_agent tool asked — and | 189 | // calls) comes from another agent — the one the ask_agent tool asked — and |
modified
internal/ui/ui_test.go +31 -0 | @@ -85,3 +85,34 @@ func TestNoticeGoesToTheSinkOrToOut(t *testing.T) { | ||
| 85 | 85 | t.Errorf("window saw %v", window.seen) |
| 86 | 86 | } |
| 87 | 87 | } |
| 88 | + | |
| 89 | +// displayingSink is a front end that shows hook displays itself. | |
| 90 | +type displayingSink struct { | |
| 91 | + stubSink | |
| 92 | + seen []string | |
| 93 | +} | |
| 94 | + | |
| 95 | +func (s *displayingSink) Display(text string) { s.seen = append(s.seen, text) } | |
| 96 | + | |
| 97 | +// A hook's display goes to the sink that can show it, else to Out (plain: | |
| 98 | +// the tests' Out is not a terminal) — never both. | |
| 99 | +func TestDisplayGoesToTheSinkOrToOut(t *testing.T) { | |
| 100 | + var out strings.Builder | |
| 101 | + old := Out | |
| 102 | + Out = &out | |
| 103 | + defer func() { Out = old }() | |
| 104 | + | |
| 105 | + SetActive(nil) | |
| 106 | + Display(`{"spell": "sleep"}`) | |
| 107 | + window := &displayingSink{} | |
| 108 | + SetActive(window) | |
| 109 | + Display("window block") | |
| 110 | + SetActive(nil) | |
| 111 | + | |
| 112 | + if got := out.String(); got != "{\"spell\": \"sleep\"}\n" { | |
| 113 | + t.Errorf("Out = %q", got) | |
| 114 | + } | |
| 115 | + if len(window.seen) != 1 || window.seen[0] != "window block" { | |
| 116 | + t.Errorf("window saw %v", window.seen) | |
| 117 | + } | |
| 118 | +} | |
| @@ -85,3 +85,34 @@ func TestNoticeGoesToTheSinkOrToOut(t *testing.T) { | |||
| 85 | t.Errorf("window saw %v", window.seen) | 85 | t.Errorf("window saw %v", window.seen) |
| 86 | } | 86 | } |
| 87 | } | 87 | } |
| 88 | + | ||
| 89 | +// displayingSink is a front end that shows hook displays itself. | ||
| 90 | +type displayingSink struct { | ||
| 91 | + stubSink | ||
| 92 | + seen []string | ||
| 93 | +} | ||
| 94 | + | ||
| 95 | +func (s *displayingSink) Display(text string) { s.seen = append(s.seen, text) } | ||
| 96 | + | ||
| 97 | +// A hook's display goes to the sink that can show it, else to Out (plain: | ||
| 98 | +// the tests' Out is not a terminal) — never both. | ||
| 99 | +func TestDisplayGoesToTheSinkOrToOut(t *testing.T) { | ||
| 100 | + var out strings.Builder | ||
| 101 | + old := Out | ||
| 102 | + Out = &out | ||
| 103 | + defer func() { Out = old }() | ||
| 104 | + | ||
| 105 | + SetActive(nil) | ||
| 106 | + Display(`{"spell": "sleep"}`) | ||
| 107 | + window := &displayingSink{} | ||
| 108 | + SetActive(window) | ||
| 109 | + Display("window block") | ||
| 110 | + SetActive(nil) | ||
| 111 | + | ||
| 112 | + if got := out.String(); got != "{\"spell\": \"sleep\"}\n" { | ||
| 113 | + t.Errorf("Out = %q", got) | ||
| 114 | + } | ||
| 115 | + if len(window.seen) != 1 || window.seen[0] != "window block" { | ||
| 116 | + t.Errorf("window saw %v", window.seen) | ||
| 117 | + } | ||
| 118 | +} | ||