📦 Mini Me
7e53bfe parent: 2341940 modified
.tickets/issues/0036-hooks.yaml +2 -1 | @@ -6,8 +6,9 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T06:32:48.566Z |
| 9 | -updatedAt: 2026-10-14T22:00:49.000Z | |
| 9 | +updatedAt: 2026-10-16T14:19:50.441Z | |
| 10 | 10 | labels: [] |
| 11 | +milestone: v0.5.0 | |
| 11 | 12 | epic: Loop |
| 12 | 13 | body: | |
| 13 | 14 | ## Overview |
| @@ -6,8 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T06:32:48.566Z | 8 | createdAt: 2026-09-21T06:32:48.566Z |
| 9 | -updatedAt: 2026-10-14T22:00:49.000Z | 9 | +updatedAt: 2026-10-16T14:19:50.441Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | +milestone: v0.5.0 | ||
| 11 | epic: Loop | 12 | epic: Loop |
| 12 | body: | | 13 | body: | |
| 13 | ## Overview | 14 | ## Overview |
modified
.tickets/issues/0048-add-a-builtin-tool-to-search-into-the-mm-session.yaml +2 -2 | @@ -6,9 +6,9 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-27T03:45:48.121Z |
| 9 | -updatedAt: 2026-10-16T11:48:27.806Z | |
| 9 | +updatedAt: 2026-10-17T16:33:44.187Z | |
| 10 | 10 | labels: [] |
| 11 | -milestone: v0.6.0 | |
| 11 | +milestone: v0.7.0 | |
| 12 | 12 | epic: Agent Memory |
| 13 | 13 | body: "" |
| 14 | 14 | tasks: [] |
| @@ -6,9 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-27T03:45:48.121Z | 8 | createdAt: 2026-09-27T03:45:48.121Z |
| 9 | -updatedAt: 2026-10-16T11:48:27.806Z | 9 | +updatedAt: 2026-10-17T16:33:44.187Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -milestone: v0.6.0 | 11 | +milestone: v0.7.0 |
| 12 | epic: Agent Memory | 12 | epic: Agent Memory |
| 13 | body: "" | 13 | body: "" |
| 14 | tasks: [] | 14 | tasks: [] |
modified
.tickets/issues/0049-recreate-the-readme.yaml +2 -2 | @@ -6,9 +6,9 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-28T18:44:44.821Z |
| 9 | -updatedAt: 2026-10-16T11:48:41.005Z | |
| 9 | +updatedAt: 2026-10-17T16:34:31.420Z | |
| 10 | 10 | labels: [] |
| 11 | -milestone: v0.6.0 | |
| 11 | +milestone: v0.7.0 | |
| 12 | 12 | epic: ToDo |
| 13 | 13 | body: "" |
| 14 | 14 | tasks: [] |
| @@ -6,9 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-28T18:44:44.821Z | 8 | createdAt: 2026-09-28T18:44:44.821Z |
| 9 | -updatedAt: 2026-10-16T11:48:41.005Z | 9 | +updatedAt: 2026-10-17T16:34:31.420Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -milestone: v0.6.0 | 11 | +milestone: v0.7.0 |
| 12 | epic: ToDo | 12 | epic: ToDo |
| 13 | body: "" | 13 | body: "" |
| 14 | tasks: [] | 14 | tasks: [] |
modified
.tickets/issues/0088-custom-tools-declared-in-the-agent-file-command.yaml +4 -5 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 88 |
| 3 | 3 | title: Custom tools declared in the agent file (command 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-16T12:00:00.000Z |
| 9 | -updatedAt: 2026-10-16T17:00:00.000Z | |
| 9 | +updatedAt: 2026-10-16T14:16:43.681Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | - builtin tool |
| @@ -89,7 +89,6 @@ body: | | ||
| 89 | 89 | |
| 90 | 90 | Measured once built (same bench, condition D = the real feature): **mellum2-12b 30/30 (1.3 s avg), qwen3.5-4b 30/30 (5.8 s)**, every run in ONE call, no invalid call. The French release request that failed 5/5 through the MCP stand-in succeeded 5/5; which difference matters (no `proj_` prefix, no "MCP tool from server" text, the "Give each parameter as its own field." line) was not isolated. |
| 91 | 91 | Demo: `demos/declared-tools/` (ops desk, four declared tools, every parameter type, mellum2 on DMR): 24/24 action requests in one call. Found there: an `enum` does not stop the model from silently swapping an unknown value for an allowed one ("alpha" → `beta`, 3/3) — fixed by the demo's system prompt. |
| 92 | - | |
| 93 | 92 | tasks: |
| 94 | 93 | - id: 1 |
| 95 | 94 | title: "Measure: MCP stand-in vs skill + bash, 2 models, 5 runs per case" |
| @@ -112,7 +111,7 @@ tasks: | ||
| 112 | 111 | updatedAt: 2026-10-16T17:00:00.000Z |
| 113 | 112 | closedAt: 2026-10-16T17:00:00.000Z |
| 114 | 113 | - id: 3 |
| 115 | - title: "Implement declared tools (config, tools, main) with tests" | |
| 114 | + title: Implement declared tools (config, tools, main) with tests | |
| 116 | 115 | state: closed |
| 117 | 116 | priority: high |
| 118 | 117 | author: |
| @@ -132,7 +131,7 @@ tasks: | ||
| 132 | 131 | updatedAt: 2026-10-16T17:00:00.000Z |
| 133 | 132 | closedAt: 2026-10-16T17:00:00.000Z |
| 134 | 133 | - id: 5 |
| 135 | - title: "Quality gate (qlty not installed in the 2026-10-16 sandbox — to run)" | |
| 134 | + title: Quality gate (qlty not installed in the 2026-10-16 sandbox — to run) | |
| 136 | 135 | state: open |
| 137 | 136 | priority: medium |
| 138 | 137 | author: |
| @@ -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: 88 | 2 | id: 88 |
| 3 | title: Custom tools declared in the agent file (command tools) | 3 | title: Custom tools declared in the agent file (command 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-16T12:00:00.000Z | 8 | createdAt: 2026-10-16T12:00:00.000Z |
| 9 | -updatedAt: 2026-10-16T17:00:00.000Z | 9 | +updatedAt: 2026-10-16T14:16:43.681Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | - builtin tool | 12 | - builtin tool |
| @@ -89,7 +89,6 @@ body: | | |||
| 89 | 89 | ||
| 90 | Measured once built (same bench, condition D = the real feature): **mellum2-12b 30/30 (1.3 s avg), qwen3.5-4b 30/30 (5.8 s)**, every run in ONE call, no invalid call. The French release request that failed 5/5 through the MCP stand-in succeeded 5/5; which difference matters (no `proj_` prefix, no "MCP tool from server" text, the "Give each parameter as its own field." line) was not isolated. | 90 | Measured once built (same bench, condition D = the real feature): **mellum2-12b 30/30 (1.3 s avg), qwen3.5-4b 30/30 (5.8 s)**, every run in ONE call, no invalid call. The French release request that failed 5/5 through the MCP stand-in succeeded 5/5; which difference matters (no `proj_` prefix, no "MCP tool from server" text, the "Give each parameter as its own field." line) was not isolated. |
| 91 | Demo: `demos/declared-tools/` (ops desk, four declared tools, every parameter type, mellum2 on DMR): 24/24 action requests in one call. Found there: an `enum` does not stop the model from silently swapping an unknown value for an allowed one ("alpha" → `beta`, 3/3) — fixed by the demo's system prompt. | 91 | Demo: `demos/declared-tools/` (ops desk, four declared tools, every parameter type, mellum2 on DMR): 24/24 action requests in one call. Found there: an `enum` does not stop the model from silently swapping an unknown value for an allowed one ("alpha" → `beta`, 3/3) — fixed by the demo's system prompt. |
| 92 | - | ||
| 93 | tasks: | 92 | tasks: |
| 94 | - id: 1 | 93 | - id: 1 |
| 95 | title: "Measure: MCP stand-in vs skill + bash, 2 models, 5 runs per case" | 94 | title: "Measure: MCP stand-in vs skill + bash, 2 models, 5 runs per case" |
| @@ -112,7 +111,7 @@ tasks: | |||
| 112 | updatedAt: 2026-10-16T17:00:00.000Z | 111 | updatedAt: 2026-10-16T17:00:00.000Z |
| 113 | closedAt: 2026-10-16T17:00:00.000Z | 112 | closedAt: 2026-10-16T17:00:00.000Z |
| 114 | - id: 3 | 113 | - id: 3 |
| 115 | - title: "Implement declared tools (config, tools, main) with tests" | 114 | + title: Implement declared tools (config, tools, main) with tests |
| 116 | state: closed | 115 | state: closed |
| 117 | priority: high | 116 | priority: high |
| 118 | author: | 117 | author: |
| @@ -132,7 +131,7 @@ tasks: | |||
| 132 | updatedAt: 2026-10-16T17:00:00.000Z | 131 | updatedAt: 2026-10-16T17:00:00.000Z |
| 133 | closedAt: 2026-10-16T17:00:00.000Z | 132 | closedAt: 2026-10-16T17:00:00.000Z |
| 134 | - id: 5 | 133 | - id: 5 |
| 135 | - title: "Quality gate (qlty not installed in the 2026-10-16 sandbox — to run)" | 134 | + title: Quality gate (qlty not installed in the 2026-10-16 sandbox — to run) |
| 136 | state: open | 135 | state: open |
| 137 | priority: medium | 136 | priority: medium |
| 138 | author: | 137 | author: |
added
.tickets/issues/0089-autonomous-agent.yaml +19 -0 | new file mode 100644 | ||
| @@ -0,0 +1,19 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 89 | |
| 3 | +title: Autonomous Agent? | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-16T15:36:08.133Z | |
| 9 | +updatedAt: 2026-10-16T15:38:22.791Z | |
| 10 | +labels: [] | |
| 11 | +milestone: v0.7.0 | |
| 12 | +related: | |
| 13 | + - 90 | |
| 14 | +body: | | |
| 15 | + Without Human interaction | |
| 16 | + | |
| 17 | + And then, what about orchestration | |
| 18 | +tasks: [] | |
| 19 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 89 | ||
| 3 | +title: Autonomous Agent? | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-16T15:36:08.133Z | ||
| 9 | +updatedAt: 2026-10-16T15:38:22.791Z | ||
| 10 | +labels: [] | ||
| 11 | +milestone: v0.7.0 | ||
| 12 | +related: | ||
| 13 | + - 90 | ||
| 14 | +body: | | ||
| 15 | + Without Human interaction | ||
| 16 | + | ||
| 17 | + And then, what about orchestration | ||
| 18 | +tasks: [] | ||
| 19 | +comments: [] | ||
added
.tickets/issues/0090-agent-identity.yaml +16 -0 | new file mode 100644 | ||
| @@ -0,0 +1,16 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 90 | |
| 3 | +title: Agent identity | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-16T15:37:32.062Z | |
| 9 | +updatedAt: 2026-10-16T15:41:47.728Z | |
| 10 | +labels: [] | |
| 11 | +milestone: v0.7.0 | |
| 12 | +body: | | |
| 13 | + Des idées pour gérer cela ? | |
| 14 | + avec l'existant (est ce que un seul useur peut avoir plusieurs identités sur GitHub par exemple) | |
| 15 | +tasks: [] | |
| 16 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 90 | ||
| 3 | +title: Agent identity | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-16T15:37:32.062Z | ||
| 9 | +updatedAt: 2026-10-16T15:41:47.728Z | ||
| 10 | +labels: [] | ||
| 11 | +milestone: v0.7.0 | ||
| 12 | +body: | | ||
| 13 | + Des idées pour gérer cela ? | ||
| 14 | + avec l'existant (est ce que un seul useur peut avoir plusieurs identités sur GitHub par exemple) | ||
| 15 | +tasks: [] | ||
| 16 | +comments: [] | ||
added
.tickets/issues/0091-plan-update-without-status-loops-the-model.yaml +72 -0 | new file mode 100644 | ||
| @@ -0,0 +1,72 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 91 | |
| 3 | +title: plan update without status loops the model | |
| 4 | +state: closed | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-17T12:00:00.000Z | |
| 9 | +updatedAt: 2026-10-17T16:33:17.797Z | |
| 10 | +labels: | |
| 11 | + - kind::bug | |
| 12 | + - builtin tool | |
| 13 | +milestone: v0.6.0 | |
| 14 | +epic: Builtin tools | |
| 15 | +body: | | |
| 16 | + ## What happened (2026-10-17) | |
| 17 | + Tuning `wip/agent.yaml` for Mellum2.1-12B-A2.5B-Thinking on llmman, the request "Écris un tutoriel complet sur Nu shell dans le fichier nushell-tutorial.md" failed 1 run out of 5: after one `plan create`, the model sent `plan update` about **30 times** until `maxTurns: 40` stopped the turn (9 min 20, `exceeded maximum tool call iterations (40)`). Every call looked like this (from the trace): | |
| 18 | + ```json | |
| 19 | + {"action":"update","name":"user-request-1","taskId":1, | |
| 20 | + "tasks":[{"description":"Write introduction in French about Nu shell","id":1}, …]} | |
| 21 | + ``` | |
| 22 | + The model was trying to **edit the plan's tasks**. `update` only changes one task's status, and `tasks` is only read by `create`. | |
| 23 | + | |
| 24 | + ## Why the model could not get out | |
| 25 | + - The answer was `[error: invalid status ''. Must be one of: open, in_progress, done, failed]` (`PlanStore.UpdateTask`, `internal/tools/plan.go`). It does not say that the `status` FIELD is missing, nor that `tasks` is ignored, nor how to change a plan's tasks (there is no such action: delete + create). | |
| 26 | + - The loop detector never fired: the `tasks` list grew a little on most calls, so no three calls in a row were identical. Only `maxTurns` ended it. | |
| 27 | + | |
| 28 | + ## What was changed | |
| 29 | + - `internal/tools/plan_actions.go`: `planUpdate` checks the status against `planStatuses` BEFORE the store, and `invalidUpdate` answers instead, e.g.: | |
| 30 | + ``` | |
| 31 | + [error: plan update changes the status of ONE task, and "status" is missing. You sent: {"action":"update","name":"user-request-1","tasks":[…],"taskId":1}. "status" must be one of: open, in_progress, done, failed. "tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again. A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}] | |
| 32 | + ``` | |
| 33 | + An unknown status (`"finished"`) gets the same shape (`"finished" is not a status`); the sentence about `tasks` only when tasks were sent; the example uses the call's name and taskId, or `user-request-1` / `1` when absent. The plan is not modified. Same principle as the declared tools' refusals (ticket 88): what was sent, what is missing, a call that works. | |
| 34 | + - Tests: `TestPlanUpdateWithoutStatusExplainsTheCall` (the traced call, plan left untouched), `TestPlanUpdateWithAnUnknownStatus`. | |
| 35 | + - Docs EN+FR: `how-to/manage-plans-and-tasks.md`, troubleshooting entry "The agent keeps calling `plan update`". | |
| 36 | + - `wip/agent.yaml` keeps `planTool: false` (writing a document needs no plan). | |
| 37 | + | |
| 38 | + ## Not done | |
| 39 | + - Not re-measured live with Mellum2.1 and `planTool: true`: the new message is unit-tested, not proven to stop the loop on the model. | |
| 40 | + - The loop detector still ignores near-identical calls; that is a separate question. | |
| 41 | + - No way to edit a plan's tasks was added. | |
| 42 | +tasks: | |
| 43 | + - id: 1 | |
| 44 | + title: Explicit refusal for update without a valid status, with tests | |
| 45 | + state: closed | |
| 46 | + priority: high | |
| 47 | + author: | |
| 48 | + name: k33g | |
| 49 | + email: ph.charriere@gmail.com | |
| 50 | + createdAt: 2026-10-17T12:00:00.000Z | |
| 51 | + updatedAt: 2026-10-17T12:00:00.000Z | |
| 52 | + closedAt: 2026-10-17T12:00:00.000Z | |
| 53 | + - id: 2 | |
| 54 | + title: "Docs EN+FR: troubleshooting entry in manage-plans-and-tasks" | |
| 55 | + state: closed | |
| 56 | + priority: medium | |
| 57 | + author: | |
| 58 | + name: k33g | |
| 59 | + email: ph.charriere@gmail.com | |
| 60 | + createdAt: 2026-10-17T12:00:00.000Z | |
| 61 | + updatedAt: 2026-10-17T12:00:00.000Z | |
| 62 | + closedAt: 2026-10-17T12:00:00.000Z | |
| 63 | + - id: 3 | |
| 64 | + title: "Re-measure Mellum2.1 with planTool: true on the tutorial request" | |
| 65 | + state: open | |
| 66 | + priority: medium | |
| 67 | + author: | |
| 68 | + name: k33g | |
| 69 | + email: ph.charriere@gmail.com | |
| 70 | + createdAt: 2026-10-17T12:00:00.000Z | |
| 71 | + updatedAt: 2026-10-17T12:00:00.000Z | |
| 72 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 91 | ||
| 3 | +title: plan update without status loops the model | ||
| 4 | +state: closed | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-17T12:00:00.000Z | ||
| 9 | +updatedAt: 2026-10-17T16:33:17.797Z | ||
| 10 | +labels: | ||
| 11 | + - kind::bug | ||
| 12 | + - builtin tool | ||
| 13 | +milestone: v0.6.0 | ||
| 14 | +epic: Builtin tools | ||
| 15 | +body: | | ||
| 16 | + ## What happened (2026-10-17) | ||
| 17 | + Tuning `wip/agent.yaml` for Mellum2.1-12B-A2.5B-Thinking on llmman, the request "Écris un tutoriel complet sur Nu shell dans le fichier nushell-tutorial.md" failed 1 run out of 5: after one `plan create`, the model sent `plan update` about **30 times** until `maxTurns: 40` stopped the turn (9 min 20, `exceeded maximum tool call iterations (40)`). Every call looked like this (from the trace): | ||
| 18 | + ```json | ||
| 19 | + {"action":"update","name":"user-request-1","taskId":1, | ||
| 20 | + "tasks":[{"description":"Write introduction in French about Nu shell","id":1}, …]} | ||
| 21 | + ``` | ||
| 22 | + The model was trying to **edit the plan's tasks**. `update` only changes one task's status, and `tasks` is only read by `create`. | ||
| 23 | + | ||
| 24 | + ## Why the model could not get out | ||
| 25 | + - The answer was `[error: invalid status ''. Must be one of: open, in_progress, done, failed]` (`PlanStore.UpdateTask`, `internal/tools/plan.go`). It does not say that the `status` FIELD is missing, nor that `tasks` is ignored, nor how to change a plan's tasks (there is no such action: delete + create). | ||
| 26 | + - The loop detector never fired: the `tasks` list grew a little on most calls, so no three calls in a row were identical. Only `maxTurns` ended it. | ||
| 27 | + | ||
| 28 | + ## What was changed | ||
| 29 | + - `internal/tools/plan_actions.go`: `planUpdate` checks the status against `planStatuses` BEFORE the store, and `invalidUpdate` answers instead, e.g.: | ||
| 30 | + ``` | ||
| 31 | + [error: plan update changes the status of ONE task, and "status" is missing. You sent: {"action":"update","name":"user-request-1","tasks":[…],"taskId":1}. "status" must be one of: open, in_progress, done, failed. "tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again. A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}] | ||
| 32 | + ``` | ||
| 33 | + An unknown status (`"finished"`) gets the same shape (`"finished" is not a status`); the sentence about `tasks` only when tasks were sent; the example uses the call's name and taskId, or `user-request-1` / `1` when absent. The plan is not modified. Same principle as the declared tools' refusals (ticket 88): what was sent, what is missing, a call that works. | ||
| 34 | + - Tests: `TestPlanUpdateWithoutStatusExplainsTheCall` (the traced call, plan left untouched), `TestPlanUpdateWithAnUnknownStatus`. | ||
| 35 | + - Docs EN+FR: `how-to/manage-plans-and-tasks.md`, troubleshooting entry "The agent keeps calling `plan update`". | ||
| 36 | + - `wip/agent.yaml` keeps `planTool: false` (writing a document needs no plan). | ||
| 37 | + | ||
| 38 | + ## Not done | ||
| 39 | + - Not re-measured live with Mellum2.1 and `planTool: true`: the new message is unit-tested, not proven to stop the loop on the model. | ||
| 40 | + - The loop detector still ignores near-identical calls; that is a separate question. | ||
| 41 | + - No way to edit a plan's tasks was added. | ||
| 42 | +tasks: | ||
| 43 | + - id: 1 | ||
| 44 | + title: Explicit refusal for update without a valid status, with tests | ||
| 45 | + state: closed | ||
| 46 | + priority: high | ||
| 47 | + author: | ||
| 48 | + name: k33g | ||
| 49 | + email: ph.charriere@gmail.com | ||
| 50 | + createdAt: 2026-10-17T12:00:00.000Z | ||
| 51 | + updatedAt: 2026-10-17T12:00:00.000Z | ||
| 52 | + closedAt: 2026-10-17T12:00:00.000Z | ||
| 53 | + - id: 2 | ||
| 54 | + title: "Docs EN+FR: troubleshooting entry in manage-plans-and-tasks" | ||
| 55 | + state: closed | ||
| 56 | + priority: medium | ||
| 57 | + author: | ||
| 58 | + name: k33g | ||
| 59 | + email: ph.charriere@gmail.com | ||
| 60 | + createdAt: 2026-10-17T12:00:00.000Z | ||
| 61 | + updatedAt: 2026-10-17T12:00:00.000Z | ||
| 62 | + closedAt: 2026-10-17T12:00:00.000Z | ||
| 63 | + - id: 3 | ||
| 64 | + title: "Re-measure Mellum2.1 with planTool: true on the tutorial request" | ||
| 65 | + state: open | ||
| 66 | + priority: medium | ||
| 67 | + author: | ||
| 68 | + name: k33g | ||
| 69 | + email: ph.charriere@gmail.com | ||
| 70 | + createdAt: 2026-10-17T12:00:00.000Z | ||
| 71 | + updatedAt: 2026-10-17T12:00:00.000Z | ||
| 72 | +comments: [] | ||
added
.tickets/issues/0092-how-to-unload-a-model.yaml +15 -0 | new file mode 100644 | ||
| @@ -0,0 +1,15 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 92 | |
| 3 | +title: How to unload a model | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-17T14:24:18.694Z | |
| 9 | +updatedAt: 2026-10-17T14:24:34.591Z | |
| 10 | +labels: | |
| 11 | + - priority::urgent | |
| 12 | +milestone: v0.7.0 | |
| 13 | +body: "" | |
| 14 | +tasks: [] | |
| 15 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 92 | ||
| 3 | +title: How to unload a model | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-17T14:24:18.694Z | ||
| 9 | +updatedAt: 2026-10-17T14:24:34.591Z | ||
| 10 | +labels: | ||
| 11 | + - priority::urgent | ||
| 12 | +milestone: v0.7.0 | ||
| 13 | +body: "" | ||
| 14 | +tasks: [] | ||
| 15 | +comments: [] | ||
added
.tickets/issues/0093-save-an-answer-without-regenerating-it.yaml +79 -0 | new file mode 100644 | ||
| @@ -0,0 +1,79 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 93 | |
| 3 | +title: Save an answer to a file without regenerating it (/save, save_answer) | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-17T16:00:00.000Z | |
| 9 | +updatedAt: 2026-10-17T16:00:00.000Z | |
| 10 | +labels: | |
| 11 | + - kind::feature | |
| 12 | + - builtin tool | |
| 13 | +epic: Builtin tools | |
| 14 | +body: | | |
| 15 | + ## Why | |
| 16 | + For an agent that answers questions with mini tutorials (user's idea, 2026-10-17: "comment faire mon 1er script nushell", "créer une lib en Go", "apprends-moi le pattern matching en Kotlin"…), the answer lives in the conversation and goes to a Markdown file ONLY when the user asks. | |
| 17 | + Saving through `write_file` makes the model generate the whole text a second time. Measured on Mellum2.1-12B-A2.5B-Thinking (llmman, `wip/` bench): the `write_file` call of a ~1,000-word tutorial cost 10,000–16,000 generated tokens, 5 to 8 minutes — the model drafts the text in its reasoning, then writes it again. The same request answered in the conversation took 1 min 45. | |
| 18 | + | |
| 19 | + ## User's decisions (2026-10-17) | |
| 20 | + - Both mechanisms: a `/save` command AND a `save_answer` tool. | |
| 21 | + - Never overwrite an existing file: refuse and say so. | |
| 22 | + - Directly on `main`. | |
| 23 | + | |
| 24 | + ## What was built | |
| 25 | + - `internal/saved` (new package): `Write` (creates parent dirs, adds the final newline, `O_EXCL` — `ErrExists` on an existing file), `ParseArg` (`<file> [n]`), `Pick`, `Command` (the one-line result), `Answers` (the model's text answers of a history — moved from `agent.modelAnswers`, now shared by /copy, /save and the tool). | |
| 26 | + - `/save <file> [n]` in the terminal and the window (`internal/command`, `agent.go`, `tui/commands.go` + `tui/copy.go` `saveAnswer`). Not offered in ACP, like /copy. The model is not called. | |
| 27 | + - `save_answer(path)` tool (`internal/tools/saveanswer.go`), behind `saveAnswerTool` (default `false`, so existing agents' tool lists do not change). It saves the text of the model message that made the call when it has text, else the last answer of the history at the start of the turn. Consent like `write_file` (yolo). Result quotes the first line saved. | |
| 28 | + - `internal/engine/reply.go`: `Reply(ctx)` — Generate's WrapTool hook puts the model message that made the call in the tool's context (the existing `Conversation(ctx)` only holds the history at the start of the turn). | |
| 29 | + - `save_answer` added to `config.BuiltinToolNames`. | |
| 30 | + - Tests: `internal/saved` (3), `tools/saveanswer_test.go` (4), `engine` `TestToolsSeeTheReplyThatCalledThem`, `tui` `TestRunnerSave`; `command` and `tui` picker tests updated for the 9th command. | |
| 31 | + - Docs EN+FR: `how-to/save-an-answer.md` (new), `reference/slash-commands.md`, `reference/configuration.md` (`saveAnswerTool`, reserved names), `reference/tools.md` (`save_answer`), both READMEs; `docs/diagrams/packages.drawio` (internal/saved node, edges from agent, tui, tools). | |
| 32 | + | |
| 33 | + ## Known limit | |
| 34 | + When the model writes a short sentence beside the call ("I'll save it."), that sentence is what gets saved. The tool description tells the model to call it alone to save a previous answer; to be measured with the mini-tutorial agent (`wip2/`). | |
| 35 | + ## Measured with the mini-tutorial agent (wip2/, 2026-10-17) | |
| 36 | + 8 cases × 2 runs, three configs (Mellum2.1 thinking 13/16, Mellum2.1 no thinking 11/16, Qwen3.5-9B 14/16). **"save it" after the answer: 6/6** — the file is the exact answer, in seconds. **"explain X and save it" in one request: 0/6**: Mellum2.1 thinking wrote the call as TEXT with wrong tags (`<tools>{…}</tools>`), Qwen3.5-9B never called the tool, Mellum2.1 without thinking called it FIRST ("no answer yet"), then once claimed it was saved, once saved its own 1,536-word deliberation + tutorial. Open: a way to save the answer being written (see wip2/README). | |
| 37 | + | |
| 38 | +tasks: | |
| 39 | + - id: 1 | |
| 40 | + title: "internal/saved + /save command, with tests" | |
| 41 | + state: closed | |
| 42 | + priority: high | |
| 43 | + author: | |
| 44 | + name: k33g | |
| 45 | + email: ph.charriere@gmail.com | |
| 46 | + createdAt: 2026-10-17T16:00:00.000Z | |
| 47 | + updatedAt: 2026-10-17T16:00:00.000Z | |
| 48 | + closedAt: 2026-10-17T16:00:00.000Z | |
| 49 | + - id: 2 | |
| 50 | + title: "save_answer tool + engine.Reply, with tests" | |
| 51 | + state: closed | |
| 52 | + priority: high | |
| 53 | + author: | |
| 54 | + name: k33g | |
| 55 | + email: ph.charriere@gmail.com | |
| 56 | + createdAt: 2026-10-17T16:00:00.000Z | |
| 57 | + updatedAt: 2026-10-17T16:00:00.000Z | |
| 58 | + closedAt: 2026-10-17T16:00:00.000Z | |
| 59 | + - id: 3 | |
| 60 | + title: "Docs EN+FR, diagram" | |
| 61 | + state: closed | |
| 62 | + priority: medium | |
| 63 | + author: | |
| 64 | + name: k33g | |
| 65 | + email: ph.charriere@gmail.com | |
| 66 | + createdAt: 2026-10-17T16:00:00.000Z | |
| 67 | + updatedAt: 2026-10-17T16:00:00.000Z | |
| 68 | + closedAt: 2026-10-17T16:00:00.000Z | |
| 69 | + - id: 4 | |
| 70 | + title: "Measure save_answer live with the mini-tutorial agent (wip2/)" | |
| 71 | + state: closed | |
| 72 | + priority: medium | |
| 73 | + author: | |
| 74 | + name: k33g | |
| 75 | + email: ph.charriere@gmail.com | |
| 76 | + createdAt: 2026-10-17T16:00:00.000Z | |
| 77 | + updatedAt: 2026-10-17T18:00:00.000Z | |
| 78 | + closedAt: 2026-10-17T18:00:00.000Z | |
| 79 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 93 | ||
| 3 | +title: Save an answer to a file without regenerating it (/save, save_answer) | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-17T16:00:00.000Z | ||
| 9 | +updatedAt: 2026-10-17T16:00:00.000Z | ||
| 10 | +labels: | ||
| 11 | + - kind::feature | ||
| 12 | + - builtin tool | ||
| 13 | +epic: Builtin tools | ||
| 14 | +body: | | ||
| 15 | + ## Why | ||
| 16 | + For an agent that answers questions with mini tutorials (user's idea, 2026-10-17: "comment faire mon 1er script nushell", "créer une lib en Go", "apprends-moi le pattern matching en Kotlin"…), the answer lives in the conversation and goes to a Markdown file ONLY when the user asks. | ||
| 17 | + Saving through `write_file` makes the model generate the whole text a second time. Measured on Mellum2.1-12B-A2.5B-Thinking (llmman, `wip/` bench): the `write_file` call of a ~1,000-word tutorial cost 10,000–16,000 generated tokens, 5 to 8 minutes — the model drafts the text in its reasoning, then writes it again. The same request answered in the conversation took 1 min 45. | ||
| 18 | + | ||
| 19 | + ## User's decisions (2026-10-17) | ||
| 20 | + - Both mechanisms: a `/save` command AND a `save_answer` tool. | ||
| 21 | + - Never overwrite an existing file: refuse and say so. | ||
| 22 | + - Directly on `main`. | ||
| 23 | + | ||
| 24 | + ## What was built | ||
| 25 | + - `internal/saved` (new package): `Write` (creates parent dirs, adds the final newline, `O_EXCL` — `ErrExists` on an existing file), `ParseArg` (`<file> [n]`), `Pick`, `Command` (the one-line result), `Answers` (the model's text answers of a history — moved from `agent.modelAnswers`, now shared by /copy, /save and the tool). | ||
| 26 | + - `/save <file> [n]` in the terminal and the window (`internal/command`, `agent.go`, `tui/commands.go` + `tui/copy.go` `saveAnswer`). Not offered in ACP, like /copy. The model is not called. | ||
| 27 | + - `save_answer(path)` tool (`internal/tools/saveanswer.go`), behind `saveAnswerTool` (default `false`, so existing agents' tool lists do not change). It saves the text of the model message that made the call when it has text, else the last answer of the history at the start of the turn. Consent like `write_file` (yolo). Result quotes the first line saved. | ||
| 28 | + - `internal/engine/reply.go`: `Reply(ctx)` — Generate's WrapTool hook puts the model message that made the call in the tool's context (the existing `Conversation(ctx)` only holds the history at the start of the turn). | ||
| 29 | + - `save_answer` added to `config.BuiltinToolNames`. | ||
| 30 | + - Tests: `internal/saved` (3), `tools/saveanswer_test.go` (4), `engine` `TestToolsSeeTheReplyThatCalledThem`, `tui` `TestRunnerSave`; `command` and `tui` picker tests updated for the 9th command. | ||
| 31 | + - Docs EN+FR: `how-to/save-an-answer.md` (new), `reference/slash-commands.md`, `reference/configuration.md` (`saveAnswerTool`, reserved names), `reference/tools.md` (`save_answer`), both READMEs; `docs/diagrams/packages.drawio` (internal/saved node, edges from agent, tui, tools). | ||
| 32 | + | ||
| 33 | + ## Known limit | ||
| 34 | + When the model writes a short sentence beside the call ("I'll save it."), that sentence is what gets saved. The tool description tells the model to call it alone to save a previous answer; to be measured with the mini-tutorial agent (`wip2/`). | ||
| 35 | + ## Measured with the mini-tutorial agent (wip2/, 2026-10-17) | ||
| 36 | + 8 cases × 2 runs, three configs (Mellum2.1 thinking 13/16, Mellum2.1 no thinking 11/16, Qwen3.5-9B 14/16). **"save it" after the answer: 6/6** — the file is the exact answer, in seconds. **"explain X and save it" in one request: 0/6**: Mellum2.1 thinking wrote the call as TEXT with wrong tags (`<tools>{…}</tools>`), Qwen3.5-9B never called the tool, Mellum2.1 without thinking called it FIRST ("no answer yet"), then once claimed it was saved, once saved its own 1,536-word deliberation + tutorial. Open: a way to save the answer being written (see wip2/README). | ||
| 37 | + | ||
| 38 | +tasks: | ||
| 39 | + - id: 1 | ||
| 40 | + title: "internal/saved + /save command, with tests" | ||
| 41 | + state: closed | ||
| 42 | + priority: high | ||
| 43 | + author: | ||
| 44 | + name: k33g | ||
| 45 | + email: ph.charriere@gmail.com | ||
| 46 | + createdAt: 2026-10-17T16:00:00.000Z | ||
| 47 | + updatedAt: 2026-10-17T16:00:00.000Z | ||
| 48 | + closedAt: 2026-10-17T16:00:00.000Z | ||
| 49 | + - id: 2 | ||
| 50 | + title: "save_answer tool + engine.Reply, with tests" | ||
| 51 | + state: closed | ||
| 52 | + priority: high | ||
| 53 | + author: | ||
| 54 | + name: k33g | ||
| 55 | + email: ph.charriere@gmail.com | ||
| 56 | + createdAt: 2026-10-17T16:00:00.000Z | ||
| 57 | + updatedAt: 2026-10-17T16:00:00.000Z | ||
| 58 | + closedAt: 2026-10-17T16:00:00.000Z | ||
| 59 | + - id: 3 | ||
| 60 | + title: "Docs EN+FR, diagram" | ||
| 61 | + state: closed | ||
| 62 | + priority: medium | ||
| 63 | + author: | ||
| 64 | + name: k33g | ||
| 65 | + email: ph.charriere@gmail.com | ||
| 66 | + createdAt: 2026-10-17T16:00:00.000Z | ||
| 67 | + updatedAt: 2026-10-17T16:00:00.000Z | ||
| 68 | + closedAt: 2026-10-17T16:00:00.000Z | ||
| 69 | + - id: 4 | ||
| 70 | + title: "Measure save_answer live with the mini-tutorial agent (wip2/)" | ||
| 71 | + state: closed | ||
| 72 | + priority: medium | ||
| 73 | + author: | ||
| 74 | + name: k33g | ||
| 75 | + email: ph.charriere@gmail.com | ||
| 76 | + createdAt: 2026-10-17T16:00:00.000Z | ||
| 77 | + updatedAt: 2026-10-17T18:00:00.000Z | ||
| 78 | + closedAt: 2026-10-17T18:00:00.000Z | ||
| 79 | +comments: [] | ||
modified
.tickets/milestones.yaml +2 -0 | @@ -24,3 +24,5 @@ milestones: | ||
| 24 | 24 | - title: v0.6.0 |
| 25 | 25 | state: open |
| 26 | 26 | dueOn: 2026-10-28 |
| 27 | + - title: v0.7.0 | |
| 28 | + state: open | |
| @@ -24,3 +24,5 @@ milestones: | |||
| 24 | - title: v0.6.0 | 24 | - title: v0.6.0 |
| 25 | state: open | 25 | state: open |
| 26 | dueOn: 2026-10-28 | 26 | dueOn: 2026-10-28 |
| 27 | + - title: v0.7.0 | ||
| 28 | + state: open | ||
added
demos/programming-answers-2/README.en.md +70 -0 | new file mode 100644 | ||
| @@ -0,0 +1,70 @@ | ||
| 1 | +# Tuto — an agent that answers with mini tutorials | |
| 2 | + | |
| 3 | +*[Version française](README.fr.md)* | |
| 4 | + | |
| 5 | +"How do I write my first Nushell script?", "how do I create a Go library?", "teach me pattern matching in Kotlin": Tuto answers with a **mini tutorial in the conversation**, and saves it to a Markdown file **only when asked**. Model: **Mellum2.1-12B-A2.5B-Thinking** (`hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M`) on **llmman**. | |
| 6 | + | |
| 7 | +## Run | |
| 8 | + | |
| 9 | +```bash | |
| 10 | +cd wip2 | |
| 11 | +mm # the terminal; mm -tui for the window | |
| 12 | +``` | |
| 13 | + | |
| 14 | +Then, in the conversation: | |
| 15 | + | |
| 16 | +``` | |
| 17 | +> How do I write my first Nushell script? | |
| 18 | +> and in bash? | |
| 19 | +> save it in first-script.md | |
| 20 | +> /save first-nushell-script.md 2 | |
| 21 | +``` | |
| 22 | + | |
| 23 | +## How it is built | |
| 24 | + | |
| 25 | +- **The answer stays in the conversation.** In `wip/`, writing a tutorial to a file cost 10,000–16,000 tokens and 5 to 8 minutes, because the model drafts the text in its reasoning and then writes it again in the `write_file` call. Answering directly costs a single draft. | |
| 26 | +- **Saving regenerates nothing.** `save_answer` (tool, `saveAnswerTool: true`) receives only a file name, and mm writes the answer already given itself. `/save <file> [n]` does the same without the model. Neither overwrites an existing file. | |
| 27 | +- **Docs only when needed.** The prompt asks for one documentation page (`fetch_content`) for a recent or niche tool, a specific version, or when in doubt, and none for well-known topics. | |
| 28 | +- **A fixed format.** A title, a goal sentence, prerequisites, 3 to 6 steps with code and expected output, pitfalls, "to go further". Between 300 and 600 words, in the language of the question. | |
| 29 | +- **Nothing else.** No `bash`, file tools, `plan`, `ask_user` or skills: on this model, every unneeded tool has been a chance to wander (see `wip/`). | |
| 30 | +- **The rest comes from `wip/`.** Reasoning on, no adjustable effort, `max_tokens: 16384`. | |
| 31 | + | |
| 32 | +## Measured results (2026-10-17) | |
| 33 | + | |
| 34 | +Eight cases, 2 runs each, in the REPL driven through a pseudo-terminal, on the development machine's llmman. The checks are scripted from the session file: complete answer, no file created unasked, 200 to 1,000 words, language of the question, an expected keyword (`cargo new`, `go mod init`, `when`, `json.load`…), syntax of complete Go blocks (`gofmt -e`). For saves: the file exists, has more than 150 words, and is identical to the answer. | |
| 35 | + | |
| 36 | +| Case | Mellum2.1, reasoning on (`agent.yaml`) | Mellum2.1, reasoning off | Qwen3.5-9B | | |
| 37 | +|-----|-----|-----|-----| | |
| 38 | +| first Nushell script | 2/2 (reads 3 to 6 pages) | 1/2 (76 words, no docs) | 2/2, **without reading the docs** | | |
| 39 | +| Rust application | 1/2 (192 words) | 2/2 | 2/2 | | |
| 40 | +| Go library | 2/2 | 2/2 | 2/2 | | |
| 41 | +| Kotlin pattern matching | 2/2 | 1/2 (answer in English) | 2/2 | | |
| 42 | +| JSON in Python (question in English) | 2/2 | 2/2 | 2/2 | | |
| 43 | +| follow-up ("and in Go?") | 2/2 | 1/2 | 2/2 | | |
| 44 | +| **"explain X and save it"** | **0/2** | **0/2** | **0/2** | | |
| 45 | +| "save it" after the answer | 2/2 | 2/2 | 2/2 | | |
| 46 | +| **Total** | **13/16** | 11/16 | 14/16 | | |
| 47 | +| Average duration of a conversation | 97 s | 50 s | 112 s | | |
| 48 | +| Answer length | 192–344 words | 76–935 words | 348–606 words | | |
| 49 | + | |
| 50 | +What stands out: | |
| 51 | + | |
| 52 | +- **Saving afterwards works everywhere** (6/6): the file is the exact answer, in a few seconds. | |
| 53 | +- **"Explain and save" in one request fails everywhere** (0/6), in three different ways: | |
| 54 | + - Mellum2.1 with reasoning writes the tutorial, then the call **as text**, with the wrong tags (`<tools>{"name": "save_answer", …}</tools>`): it is not a tool call; | |
| 55 | + - Qwen3.5-9B never calls the tool; | |
| 56 | + - Mellum2.1 without reasoning calls `save_answer` **before** writing anything and gets "no answer yet". In one run it then claims it was saved, which is false. In the other it calls the tool again after writing, in its reply, its whole deliberation ("I need to explain them… I can only make one tool call per turn…") followed by the tutorial: those 1,536 words are what got saved. | |
| 57 | + | |
| 58 | + This is the trap already known with small models: writing a text, then calling a tool, in the same reply. | |
| 59 | +- **Reasoning keeps the format.** With it on, Mellum2.1 follows the short format, and it is the only one to read the docs for Nushell (2/2). With it off, it is twice as fast but uneven: 76 to 935 words, an answer in English to a question in French. | |
| 60 | +- **Qwen3.5-9B is the most regular** (14/16), but it is the slowest and **never read the Nushell docs**. Its answers reach the top of the requested range (348–606 words). | |
| 61 | +- **No complete Go block had a syntax error.** The other languages were not checked: no Rust, Kotlin or Nushell in the measurement sandbox. Nor was the correctness of the Nushell commands checked. | |
| 62 | + | |
| 63 | +Two runs per case: an order of magnitude, not a guarantee. | |
| 64 | + | |
| 65 | +## Known limits | |
| 66 | + | |
| 67 | +- **"Explain and save" does not work** (see above). Meanwhile: ask the question, then ask "save it in x.md", or type `/save x.md`. | |
| 68 | +- `save_answer` saves all the text written beside the call: a short sentence ("I'll save it."), or a deliberation (seen 1 time out of 6, reasoning off). | |
| 69 | +- The prompt partly contradicts itself: "call it alone" for a previous answer, but "in that same reply" for "explain and save". The model without reasoning deliberated at length over that contradiction. | |
| 70 | +- **Not measured:** the window (`-tui`), the editor (`-acp`), long conversations (compression). | |
| new file mode 100644 | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | +# Tuto — an agent that answers with mini tutorials | ||
| 2 | + | ||
| 3 | +*[Version française](README.fr.md)* | ||
| 4 | + | ||
| 5 | +"How do I write my first Nushell script?", "how do I create a Go library?", "teach me pattern matching in Kotlin": Tuto answers with a **mini tutorial in the conversation**, and saves it to a Markdown file **only when asked**. Model: **Mellum2.1-12B-A2.5B-Thinking** (`hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M`) on **llmman**. | ||
| 6 | + | ||
| 7 | +## Run | ||
| 8 | + | ||
| 9 | +```bash | ||
| 10 | +cd wip2 | ||
| 11 | +mm # the terminal; mm -tui for the window | ||
| 12 | +``` | ||
| 13 | + | ||
| 14 | +Then, in the conversation: | ||
| 15 | + | ||
| 16 | +``` | ||
| 17 | +> How do I write my first Nushell script? | ||
| 18 | +> and in bash? | ||
| 19 | +> save it in first-script.md | ||
| 20 | +> /save first-nushell-script.md 2 | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +## How it is built | ||
| 24 | + | ||
| 25 | +- **The answer stays in the conversation.** In `wip/`, writing a tutorial to a file cost 10,000–16,000 tokens and 5 to 8 minutes, because the model drafts the text in its reasoning and then writes it again in the `write_file` call. Answering directly costs a single draft. | ||
| 26 | +- **Saving regenerates nothing.** `save_answer` (tool, `saveAnswerTool: true`) receives only a file name, and mm writes the answer already given itself. `/save <file> [n]` does the same without the model. Neither overwrites an existing file. | ||
| 27 | +- **Docs only when needed.** The prompt asks for one documentation page (`fetch_content`) for a recent or niche tool, a specific version, or when in doubt, and none for well-known topics. | ||
| 28 | +- **A fixed format.** A title, a goal sentence, prerequisites, 3 to 6 steps with code and expected output, pitfalls, "to go further". Between 300 and 600 words, in the language of the question. | ||
| 29 | +- **Nothing else.** No `bash`, file tools, `plan`, `ask_user` or skills: on this model, every unneeded tool has been a chance to wander (see `wip/`). | ||
| 30 | +- **The rest comes from `wip/`.** Reasoning on, no adjustable effort, `max_tokens: 16384`. | ||
| 31 | + | ||
| 32 | +## Measured results (2026-10-17) | ||
| 33 | + | ||
| 34 | +Eight cases, 2 runs each, in the REPL driven through a pseudo-terminal, on the development machine's llmman. The checks are scripted from the session file: complete answer, no file created unasked, 200 to 1,000 words, language of the question, an expected keyword (`cargo new`, `go mod init`, `when`, `json.load`…), syntax of complete Go blocks (`gofmt -e`). For saves: the file exists, has more than 150 words, and is identical to the answer. | ||
| 35 | + | ||
| 36 | +| Case | Mellum2.1, reasoning on (`agent.yaml`) | Mellum2.1, reasoning off | Qwen3.5-9B | | ||
| 37 | +|-----|-----|-----|-----| | ||
| 38 | +| first Nushell script | 2/2 (reads 3 to 6 pages) | 1/2 (76 words, no docs) | 2/2, **without reading the docs** | | ||
| 39 | +| Rust application | 1/2 (192 words) | 2/2 | 2/2 | | ||
| 40 | +| Go library | 2/2 | 2/2 | 2/2 | | ||
| 41 | +| Kotlin pattern matching | 2/2 | 1/2 (answer in English) | 2/2 | | ||
| 42 | +| JSON in Python (question in English) | 2/2 | 2/2 | 2/2 | | ||
| 43 | +| follow-up ("and in Go?") | 2/2 | 1/2 | 2/2 | | ||
| 44 | +| **"explain X and save it"** | **0/2** | **0/2** | **0/2** | | ||
| 45 | +| "save it" after the answer | 2/2 | 2/2 | 2/2 | | ||
| 46 | +| **Total** | **13/16** | 11/16 | 14/16 | | ||
| 47 | +| Average duration of a conversation | 97 s | 50 s | 112 s | | ||
| 48 | +| Answer length | 192–344 words | 76–935 words | 348–606 words | | ||
| 49 | + | ||
| 50 | +What stands out: | ||
| 51 | + | ||
| 52 | +- **Saving afterwards works everywhere** (6/6): the file is the exact answer, in a few seconds. | ||
| 53 | +- **"Explain and save" in one request fails everywhere** (0/6), in three different ways: | ||
| 54 | + - Mellum2.1 with reasoning writes the tutorial, then the call **as text**, with the wrong tags (`<tools>{"name": "save_answer", …}</tools>`): it is not a tool call; | ||
| 55 | + - Qwen3.5-9B never calls the tool; | ||
| 56 | + - Mellum2.1 without reasoning calls `save_answer` **before** writing anything and gets "no answer yet". In one run it then claims it was saved, which is false. In the other it calls the tool again after writing, in its reply, its whole deliberation ("I need to explain them… I can only make one tool call per turn…") followed by the tutorial: those 1,536 words are what got saved. | ||
| 57 | + | ||
| 58 | + This is the trap already known with small models: writing a text, then calling a tool, in the same reply. | ||
| 59 | +- **Reasoning keeps the format.** With it on, Mellum2.1 follows the short format, and it is the only one to read the docs for Nushell (2/2). With it off, it is twice as fast but uneven: 76 to 935 words, an answer in English to a question in French. | ||
| 60 | +- **Qwen3.5-9B is the most regular** (14/16), but it is the slowest and **never read the Nushell docs**. Its answers reach the top of the requested range (348–606 words). | ||
| 61 | +- **No complete Go block had a syntax error.** The other languages were not checked: no Rust, Kotlin or Nushell in the measurement sandbox. Nor was the correctness of the Nushell commands checked. | ||
| 62 | + | ||
| 63 | +Two runs per case: an order of magnitude, not a guarantee. | ||
| 64 | + | ||
| 65 | +## Known limits | ||
| 66 | + | ||
| 67 | +- **"Explain and save" does not work** (see above). Meanwhile: ask the question, then ask "save it in x.md", or type `/save x.md`. | ||
| 68 | +- `save_answer` saves all the text written beside the call: a short sentence ("I'll save it."), or a deliberation (seen 1 time out of 6, reasoning off). | ||
| 69 | +- The prompt partly contradicts itself: "call it alone" for a previous answer, but "in that same reply" for "explain and save". The model without reasoning deliberated at length over that contradiction. | ||
| 70 | +- **Not measured:** the window (`-tui`), the editor (`-acp`), long conversations (compression). | ||
added
demos/programming-answers-2/README.fr.md +70 -0 | new file mode 100644 | ||
| @@ -0,0 +1,70 @@ | ||
| 1 | +# Tuto — un agent qui répond par des mini-tutoriels | |
| 2 | + | |
| 3 | +*[English version](README.en.md)* | |
| 4 | + | |
| 5 | +« Comment faire mon premier script Nushell ? », « comment créer une librairie en Go ? », « apprends-moi le pattern matching en Kotlin » : Tuto répond par un **mini-tutoriel dans la conversation**, et le sauvegarde dans un fichier Markdown **seulement si on le demande**. Modèle : **Mellum2.1-12B-A2.5B-Thinking** (`hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M`) sur **llmman**. | |
| 6 | + | |
| 7 | +## Lancer | |
| 8 | + | |
| 9 | +```bash | |
| 10 | +cd wip2 | |
| 11 | +mm # le terminal ; mm -tui pour la fenêtre | |
| 12 | +``` | |
| 13 | + | |
| 14 | +Puis, dans la conversation : | |
| 15 | + | |
| 16 | +``` | |
| 17 | +> Comment faire mon premier script Nushell ? | |
| 18 | +> et en bash ? | |
| 19 | +> sauvegarde-le dans premier-script.md | |
| 20 | +> /save premier-script-nushell.md 2 | |
| 21 | +``` | |
| 22 | + | |
| 23 | +## Comment il est construit | |
| 24 | + | |
| 25 | +- **La réponse reste dans la conversation.** Dans `wip/`, écrire un tutoriel dans un fichier coûtait 10 000 à 16 000 tokens et 5 à 8 minutes, parce que le modèle rédige le texte dans son raisonnement puis le réécrit dans l'appel `write_file`. Répondre directement coûte une seule rédaction. | |
| 26 | +- **Sauvegarder ne régénère rien.** `save_answer` (tool, `saveAnswerTool: true`) ne reçoit qu'un nom de fichier, et mm écrit lui-même la réponse déjà donnée. `/save <fichier> [n]` fait la même chose sans le modèle. Aucun des deux n'écrase un fichier existant. | |
| 27 | +- **La doc seulement si nécessaire.** Le prompt demande une page de documentation (`fetch_content`) pour un outil récent ou de niche, une version précise ou en cas de doute, et rien pour les sujets connus. | |
| 28 | +- **Un format fixe.** Un titre, une phrase d'objectif, les prérequis, 3 à 6 étapes avec du code et la sortie attendue, les pièges, « pour aller plus loin ». Entre 300 et 600 mots, dans la langue de la question. | |
| 29 | +- **Rien d'autre.** Pas de `bash`, d'outils de fichiers, de `plan`, d'`ask_user` ni de skills : sur ce modèle, chaque outil inutile a été une occasion de dériver (voir `wip/`). | |
| 30 | +- **Le reste vient de `wip/`.** Raisonnement activé, aucun effort réglable, `max_tokens: 16384`. | |
| 31 | + | |
| 32 | +## Résultats mesurés (2026-10-17) | |
| 33 | + | |
| 34 | +Huit cas, 2 runs chacun, dans le REPL piloté par un pseudo-terminal, sur le llmman de la machine de développement. Les contrôles sont scriptés depuis le fichier de session : réponse complète, aucun fichier créé sans demande, 200 à 1 000 mots, langue de la question, un mot-clé attendu (`cargo new`, `go mod init`, `when`, `json.load`…), syntaxe des blocs Go complets (`gofmt -e`). Pour les sauvegardes : le fichier existe, il fait plus de 150 mots, et il est identique à la réponse. | |
| 35 | + | |
| 36 | +| Cas | Mellum2.1, raisonnement activé (`agent.yaml`) | Mellum2.1, raisonnement désactivé | Qwen3.5-9B | | |
| 37 | +|-----|-----|-----|-----| | |
| 38 | +| premier script Nushell | 2/2 (lit 3 à 6 pages) | 1/2 (76 mots, sans doc) | 2/2, **sans lire la doc** | | |
| 39 | +| application Rust | 1/2 (192 mots) | 2/2 | 2/2 | | |
| 40 | +| librairie Go | 2/2 | 2/2 | 2/2 | | |
| 41 | +| pattern matching Kotlin | 2/2 | 1/2 (réponse en anglais) | 2/2 | | |
| 42 | +| JSON en Python (question en anglais) | 2/2 | 2/2 | 2/2 | | |
| 43 | +| question de suite (« et en Go ? ») | 2/2 | 1/2 | 2/2 | | |
| 44 | +| **« explique X et sauvegarde-le »** | **0/2** | **0/2** | **0/2** | | |
| 45 | +| « sauvegarde-le » après la réponse | 2/2 | 2/2 | 2/2 | | |
| 46 | +| **Total** | **13/16** | 11/16 | 14/16 | | |
| 47 | +| Durée moyenne d'une conversation | 97 s | 50 s | 112 s | | |
| 48 | +| Longueur des réponses | 192 à 344 mots | 76 à 935 mots | 348 à 606 mots | | |
| 49 | + | |
| 50 | +Ce qui ressort : | |
| 51 | + | |
| 52 | +- **Sauvegarder après coup marche partout** (6/6) : le fichier est la réponse exacte, en quelques secondes. | |
| 53 | +- **« Explique et sauvegarde » dans une même demande échoue partout** (0/6), de trois façons différentes : | |
| 54 | + - Mellum2.1 avec raisonnement écrit le tutoriel, puis l'appel **sous forme de texte**, avec de mauvaises balises (`<tools>{"name": "save_answer", …}</tools>`) : ce n'est pas un appel d'outil ; | |
| 55 | + - Qwen3.5-9B n'appelle jamais le tool ; | |
| 56 | + - Mellum2.1 sans raisonnement appelle `save_answer` **avant** d'avoir écrit et reçoit « no answer yet ». Dans un run, il affirme ensuite que c'est sauvegardé, ce qui est faux. Dans l'autre, il rappelle le tool après avoir écrit, dans sa réponse, toute sa délibération (« I need to explain them… I can only make one tool call per turn… ») suivie du tutoriel : c'est cet ensemble de 1 536 mots qui est enregistré. | |
| 57 | + | |
| 58 | + C'est le piège déjà connu avec les petits modèles : écrire un texte, puis appeler un outil dans la même réponse. | |
| 59 | +- **Le raisonnement tient le format.** Activé, Mellum2.1 suit le format court, et c'est le seul à lire la doc pour Nushell (2/2). Désactivé, il est deux fois plus rapide mais irrégulier : de 76 à 935 mots, une réponse en anglais à une question en français. | |
| 60 | +- **Qwen3.5-9B est le plus régulier** (14/16), mais il est le plus lent et **n'a jamais lu la doc de Nushell**. Ses réponses montent jusqu'au haut de la fourchette demandée (348 à 606 mots). | |
| 61 | +- **Aucun bloc Go complet n'avait d'erreur de syntaxe.** Les autres langages n'ont pas été vérifiés : pas de Rust, Kotlin ni Nushell dans le sandbox de mesure. L'exactitude des commandes Nushell n'a pas non plus été vérifiée. | |
| 62 | + | |
| 63 | +Deux runs par cas, c'est un ordre de grandeur, pas une garantie. | |
| 64 | + | |
| 65 | +## Limites connues | |
| 66 | + | |
| 67 | +- **« Explique et sauvegarde » ne fonctionne pas** (voir ci-dessus). En attendant : posez la question, puis demandez « sauvegarde-le dans x.md », ou tapez `/save x.md`. | |
| 68 | +- `save_answer` enregistre tout le texte écrit à côté de l'appel : une courte phrase (« Je le sauvegarde. »), ou une délibération (vu 1 fois sur 6, sans raisonnement). | |
| 69 | +- Le prompt se contredit en partie : « appelle-le seul » pour une réponse précédente, mais « dans la même réponse » pour « explique et sauvegarde ». Le modèle sans raisonnement a délibéré longuement sur cette contradiction. | |
| 70 | +- **Non mesuré :** la fenêtre (`-tui`), l'éditeur (`-acp`), les conversations longues (compression). | |
| new file mode 100644 | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | +# Tuto — un agent qui répond par des mini-tutoriels | ||
| 2 | + | ||
| 3 | +*[English version](README.en.md)* | ||
| 4 | + | ||
| 5 | +« Comment faire mon premier script Nushell ? », « comment créer une librairie en Go ? », « apprends-moi le pattern matching en Kotlin » : Tuto répond par un **mini-tutoriel dans la conversation**, et le sauvegarde dans un fichier Markdown **seulement si on le demande**. Modèle : **Mellum2.1-12B-A2.5B-Thinking** (`hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M`) sur **llmman**. | ||
| 6 | + | ||
| 7 | +## Lancer | ||
| 8 | + | ||
| 9 | +```bash | ||
| 10 | +cd wip2 | ||
| 11 | +mm # le terminal ; mm -tui pour la fenêtre | ||
| 12 | +``` | ||
| 13 | + | ||
| 14 | +Puis, dans la conversation : | ||
| 15 | + | ||
| 16 | +``` | ||
| 17 | +> Comment faire mon premier script Nushell ? | ||
| 18 | +> et en bash ? | ||
| 19 | +> sauvegarde-le dans premier-script.md | ||
| 20 | +> /save premier-script-nushell.md 2 | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +## Comment il est construit | ||
| 24 | + | ||
| 25 | +- **La réponse reste dans la conversation.** Dans `wip/`, écrire un tutoriel dans un fichier coûtait 10 000 à 16 000 tokens et 5 à 8 minutes, parce que le modèle rédige le texte dans son raisonnement puis le réécrit dans l'appel `write_file`. Répondre directement coûte une seule rédaction. | ||
| 26 | +- **Sauvegarder ne régénère rien.** `save_answer` (tool, `saveAnswerTool: true`) ne reçoit qu'un nom de fichier, et mm écrit lui-même la réponse déjà donnée. `/save <fichier> [n]` fait la même chose sans le modèle. Aucun des deux n'écrase un fichier existant. | ||
| 27 | +- **La doc seulement si nécessaire.** Le prompt demande une page de documentation (`fetch_content`) pour un outil récent ou de niche, une version précise ou en cas de doute, et rien pour les sujets connus. | ||
| 28 | +- **Un format fixe.** Un titre, une phrase d'objectif, les prérequis, 3 à 6 étapes avec du code et la sortie attendue, les pièges, « pour aller plus loin ». Entre 300 et 600 mots, dans la langue de la question. | ||
| 29 | +- **Rien d'autre.** Pas de `bash`, d'outils de fichiers, de `plan`, d'`ask_user` ni de skills : sur ce modèle, chaque outil inutile a été une occasion de dériver (voir `wip/`). | ||
| 30 | +- **Le reste vient de `wip/`.** Raisonnement activé, aucun effort réglable, `max_tokens: 16384`. | ||
| 31 | + | ||
| 32 | +## Résultats mesurés (2026-10-17) | ||
| 33 | + | ||
| 34 | +Huit cas, 2 runs chacun, dans le REPL piloté par un pseudo-terminal, sur le llmman de la machine de développement. Les contrôles sont scriptés depuis le fichier de session : réponse complète, aucun fichier créé sans demande, 200 à 1 000 mots, langue de la question, un mot-clé attendu (`cargo new`, `go mod init`, `when`, `json.load`…), syntaxe des blocs Go complets (`gofmt -e`). Pour les sauvegardes : le fichier existe, il fait plus de 150 mots, et il est identique à la réponse. | ||
| 35 | + | ||
| 36 | +| Cas | Mellum2.1, raisonnement activé (`agent.yaml`) | Mellum2.1, raisonnement désactivé | Qwen3.5-9B | | ||
| 37 | +|-----|-----|-----|-----| | ||
| 38 | +| premier script Nushell | 2/2 (lit 3 à 6 pages) | 1/2 (76 mots, sans doc) | 2/2, **sans lire la doc** | | ||
| 39 | +| application Rust | 1/2 (192 mots) | 2/2 | 2/2 | | ||
| 40 | +| librairie Go | 2/2 | 2/2 | 2/2 | | ||
| 41 | +| pattern matching Kotlin | 2/2 | 1/2 (réponse en anglais) | 2/2 | | ||
| 42 | +| JSON en Python (question en anglais) | 2/2 | 2/2 | 2/2 | | ||
| 43 | +| question de suite (« et en Go ? ») | 2/2 | 1/2 | 2/2 | | ||
| 44 | +| **« explique X et sauvegarde-le »** | **0/2** | **0/2** | **0/2** | | ||
| 45 | +| « sauvegarde-le » après la réponse | 2/2 | 2/2 | 2/2 | | ||
| 46 | +| **Total** | **13/16** | 11/16 | 14/16 | | ||
| 47 | +| Durée moyenne d'une conversation | 97 s | 50 s | 112 s | | ||
| 48 | +| Longueur des réponses | 192 à 344 mots | 76 à 935 mots | 348 à 606 mots | | ||
| 49 | + | ||
| 50 | +Ce qui ressort : | ||
| 51 | + | ||
| 52 | +- **Sauvegarder après coup marche partout** (6/6) : le fichier est la réponse exacte, en quelques secondes. | ||
| 53 | +- **« Explique et sauvegarde » dans une même demande échoue partout** (0/6), de trois façons différentes : | ||
| 54 | + - Mellum2.1 avec raisonnement écrit le tutoriel, puis l'appel **sous forme de texte**, avec de mauvaises balises (`<tools>{"name": "save_answer", …}</tools>`) : ce n'est pas un appel d'outil ; | ||
| 55 | + - Qwen3.5-9B n'appelle jamais le tool ; | ||
| 56 | + - Mellum2.1 sans raisonnement appelle `save_answer` **avant** d'avoir écrit et reçoit « no answer yet ». Dans un run, il affirme ensuite que c'est sauvegardé, ce qui est faux. Dans l'autre, il rappelle le tool après avoir écrit, dans sa réponse, toute sa délibération (« I need to explain them… I can only make one tool call per turn… ») suivie du tutoriel : c'est cet ensemble de 1 536 mots qui est enregistré. | ||
| 57 | + | ||
| 58 | + C'est le piège déjà connu avec les petits modèles : écrire un texte, puis appeler un outil dans la même réponse. | ||
| 59 | +- **Le raisonnement tient le format.** Activé, Mellum2.1 suit le format court, et c'est le seul à lire la doc pour Nushell (2/2). Désactivé, il est deux fois plus rapide mais irrégulier : de 76 à 935 mots, une réponse en anglais à une question en français. | ||
| 60 | +- **Qwen3.5-9B est le plus régulier** (14/16), mais il est le plus lent et **n'a jamais lu la doc de Nushell**. Ses réponses montent jusqu'au haut de la fourchette demandée (348 à 606 mots). | ||
| 61 | +- **Aucun bloc Go complet n'avait d'erreur de syntaxe.** Les autres langages n'ont pas été vérifiés : pas de Rust, Kotlin ni Nushell dans le sandbox de mesure. L'exactitude des commandes Nushell n'a pas non plus été vérifiée. | ||
| 62 | + | ||
| 63 | +Deux runs par cas, c'est un ordre de grandeur, pas une garantie. | ||
| 64 | + | ||
| 65 | +## Limites connues | ||
| 66 | + | ||
| 67 | +- **« Explique et sauvegarde » ne fonctionne pas** (voir ci-dessus). En attendant : posez la question, puis demandez « sauvegarde-le dans x.md », ou tapez `/save x.md`. | ||
| 68 | +- `save_answer` enregistre tout le texte écrit à côté de l'appel : une courte phrase (« Je le sauvegarde. »), ou une délibération (vu 1 fois sur 6, sans raisonnement). | ||
| 69 | +- Le prompt se contredit en partie : « appelle-le seul » pour une réponse précédente, mais « dans la même réponse » pour « explique et sauvegarde ». Le modèle sans raisonnement a délibéré longuement sur cette contradiction. | ||
| 70 | +- **Non mesuré :** la fenêtre (`-tui`), l'éditeur (`-acp`), les conversations longues (compression). | ||
added
demos/programming-answers-2/agent.yaml +95 -0 | new file mode 100644 | ||
| @@ -0,0 +1,95 @@ | ||
| 1 | +# Tuto — an agent that answers with mini tutorials ("how do I write my first | |
| 2 | +# Nushell script", "create a Go library", "teach me pattern matching in | |
| 3 | +# Kotlin"…). The answer stays in the conversation; it goes to a Markdown file | |
| 4 | +# only when the user asks (save_answer, or the /save command). | |
| 5 | +# Mellum2.1 12B-A2.5B Thinking on llmman. See README.en.md / README.fr.md. | |
| 6 | +provider: llmman | |
| 7 | +model: hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M | |
| 8 | +baseUrl: http://127.0.0.1:17434/v1 | |
| 9 | +fallback: http://host.docker.internal:17434/v1 | |
| 10 | + | |
| 11 | +# mm's budget for compression; llmman served 131072 during the measurements. | |
| 12 | +contextWindow: 32768 | |
| 13 | + | |
| 14 | +sampling: | |
| 15 | + # Explanations, not code edits: a little more freedom than a coder's 0.2, | |
| 16 | + # never 0.0 with reasoning on (thinking models loop when greedy). | |
| 17 | + temperature: 0.3 | |
| 18 | + top_p: 0.9 | |
| 19 | + # Measured in wip/: 16384 holds the reasoning plus a long answer; 24576 made | |
| 20 | + # things worse (the model reasons longer). | |
| 21 | + max_tokens: 16384 | |
| 22 | + parallel_tool_calls: false | |
| 23 | + # The template's only reasoning switch (no effort level — see wip/README). | |
| 24 | + chat_template_kwargs: | |
| 25 | + enable_thinking: true | |
| 26 | + | |
| 27 | +showThinking: true | |
| 28 | +firstTokenTimeout: 3m | |
| 29 | +watchdogTimeout: 2m | |
| 30 | + | |
| 31 | +# A mini tutorial is one answer, maybe after one fetch, maybe one save. | |
| 32 | +maxTurns: 8 | |
| 33 | +maxOutput: 8000 | |
| 34 | +previewLines: 10 | |
| 35 | + | |
| 36 | +# ─── Tools: the documentation, and saving on request ───────────────────────── | |
| 37 | +yolo: true | |
| 38 | +bashTool: false | |
| 39 | +editTools: false | |
| 40 | +planTool: false | |
| 41 | +askUserTool: false | |
| 42 | +fetchContentTool: true | |
| 43 | +saveAnswerTool: true | |
| 44 | +skillsDir: no-skills | |
| 45 | + | |
| 46 | +agentsMd: | |
| 47 | + enabled: false | |
| 48 | + | |
| 49 | +sessions: | |
| 50 | + enabled: true | |
| 51 | + dir: .mm/sessions | |
| 52 | + | |
| 53 | +context: | |
| 54 | + enabled: true | |
| 55 | + threshold: 60 | |
| 56 | + keepLastTurns: 2 | |
| 57 | + summaryMaxTokens: 1200 | |
| 58 | + | |
| 59 | +acp: | |
| 60 | + name: tuto | |
| 61 | + title: 📘 Tuto | |
| 62 | + | |
| 63 | +system: | | |
| 64 | + You are Tuto. You answer questions about programming languages, shells and | |
| 65 | + developer tools with a MINI TUTORIAL, in the language of the question. | |
| 66 | + | |
| 67 | + HOW TO ANSWER | |
| 68 | + 1. Decide whether you need the documentation. Fetch ONE page with | |
| 69 | + fetch_content only when the subject is a recent or niche tool (for | |
| 70 | + example Nushell, a young framework), when the user names a version, or | |
| 71 | + when you are not sure a command or an option exists. For well-known | |
| 72 | + topics (Go, Rust, Python, Kotlin, Java basics) answer from what you know. | |
| 73 | + 2. Write the mini tutorial directly in your reply, in Markdown: | |
| 74 | + - a title (#) | |
| 75 | + - one sentence: what the reader will be able to do | |
| 76 | + - Prerequisites: a short list | |
| 77 | + - 3 to 6 numbered steps, each with a code block and, when it prints | |
| 78 | + something, the expected output | |
| 79 | + - Pitfalls: 2 or 3 short points | |
| 80 | + - To go further: one or two lines | |
| 81 | + Between 300 and 600 words. Short snippets; a complete program only when | |
| 82 | + the user asks for one. | |
| 83 | + 3. A follow-up question ("and in Go?") refers to your previous answer: | |
| 84 | + answer the same need for the new language or tool. | |
| 85 | + | |
| 86 | + SAVING TO A FILE | |
| 87 | + - Never save unless the user asks to save, write or export to a file. | |
| 88 | + - When asked, call save_answer with the file name the user gave (or a short | |
| 89 | + name made from the subject, ending in .md). Do NOT write the tutorial | |
| 90 | + again: save_answer saves your previous answer by itself — call it alone, | |
| 91 | + with no text in your reply. | |
| 92 | + - When the request is "explain X and save it", write the tutorial in your | |
| 93 | + reply AND call save_answer in that same reply. | |
| 94 | + - If save_answer says the file exists, tell the user and ask for another | |
| 95 | + name. | |
| new file mode 100644 | |||
| @@ -0,0 +1,95 @@ | |||
| 1 | +# Tuto — an agent that answers with mini tutorials ("how do I write my first | ||
| 2 | +# Nushell script", "create a Go library", "teach me pattern matching in | ||
| 3 | +# Kotlin"…). The answer stays in the conversation; it goes to a Markdown file | ||
| 4 | +# only when the user asks (save_answer, or the /save command). | ||
| 5 | +# Mellum2.1 12B-A2.5B Thinking on llmman. See README.en.md / README.fr.md. | ||
| 6 | +provider: llmman | ||
| 7 | +model: hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M | ||
| 8 | +baseUrl: http://127.0.0.1:17434/v1 | ||
| 9 | +fallback: http://host.docker.internal:17434/v1 | ||
| 10 | + | ||
| 11 | +# mm's budget for compression; llmman served 131072 during the measurements. | ||
| 12 | +contextWindow: 32768 | ||
| 13 | + | ||
| 14 | +sampling: | ||
| 15 | + # Explanations, not code edits: a little more freedom than a coder's 0.2, | ||
| 16 | + # never 0.0 with reasoning on (thinking models loop when greedy). | ||
| 17 | + temperature: 0.3 | ||
| 18 | + top_p: 0.9 | ||
| 19 | + # Measured in wip/: 16384 holds the reasoning plus a long answer; 24576 made | ||
| 20 | + # things worse (the model reasons longer). | ||
| 21 | + max_tokens: 16384 | ||
| 22 | + parallel_tool_calls: false | ||
| 23 | + # The template's only reasoning switch (no effort level — see wip/README). | ||
| 24 | + chat_template_kwargs: | ||
| 25 | + enable_thinking: true | ||
| 26 | + | ||
| 27 | +showThinking: true | ||
| 28 | +firstTokenTimeout: 3m | ||
| 29 | +watchdogTimeout: 2m | ||
| 30 | + | ||
| 31 | +# A mini tutorial is one answer, maybe after one fetch, maybe one save. | ||
| 32 | +maxTurns: 8 | ||
| 33 | +maxOutput: 8000 | ||
| 34 | +previewLines: 10 | ||
| 35 | + | ||
| 36 | +# ─── Tools: the documentation, and saving on request ───────────────────────── | ||
| 37 | +yolo: true | ||
| 38 | +bashTool: false | ||
| 39 | +editTools: false | ||
| 40 | +planTool: false | ||
| 41 | +askUserTool: false | ||
| 42 | +fetchContentTool: true | ||
| 43 | +saveAnswerTool: true | ||
| 44 | +skillsDir: no-skills | ||
| 45 | + | ||
| 46 | +agentsMd: | ||
| 47 | + enabled: false | ||
| 48 | + | ||
| 49 | +sessions: | ||
| 50 | + enabled: true | ||
| 51 | + dir: .mm/sessions | ||
| 52 | + | ||
| 53 | +context: | ||
| 54 | + enabled: true | ||
| 55 | + threshold: 60 | ||
| 56 | + keepLastTurns: 2 | ||
| 57 | + summaryMaxTokens: 1200 | ||
| 58 | + | ||
| 59 | +acp: | ||
| 60 | + name: tuto | ||
| 61 | + title: 📘 Tuto | ||
| 62 | + | ||
| 63 | +system: | | ||
| 64 | + You are Tuto. You answer questions about programming languages, shells and | ||
| 65 | + developer tools with a MINI TUTORIAL, in the language of the question. | ||
| 66 | + | ||
| 67 | + HOW TO ANSWER | ||
| 68 | + 1. Decide whether you need the documentation. Fetch ONE page with | ||
| 69 | + fetch_content only when the subject is a recent or niche tool (for | ||
| 70 | + example Nushell, a young framework), when the user names a version, or | ||
| 71 | + when you are not sure a command or an option exists. For well-known | ||
| 72 | + topics (Go, Rust, Python, Kotlin, Java basics) answer from what you know. | ||
| 73 | + 2. Write the mini tutorial directly in your reply, in Markdown: | ||
| 74 | + - a title (#) | ||
| 75 | + - one sentence: what the reader will be able to do | ||
| 76 | + - Prerequisites: a short list | ||
| 77 | + - 3 to 6 numbered steps, each with a code block and, when it prints | ||
| 78 | + something, the expected output | ||
| 79 | + - Pitfalls: 2 or 3 short points | ||
| 80 | + - To go further: one or two lines | ||
| 81 | + Between 300 and 600 words. Short snippets; a complete program only when | ||
| 82 | + the user asks for one. | ||
| 83 | + 3. A follow-up question ("and in Go?") refers to your previous answer: | ||
| 84 | + answer the same need for the new language or tool. | ||
| 85 | + | ||
| 86 | + SAVING TO A FILE | ||
| 87 | + - Never save unless the user asks to save, write or export to a file. | ||
| 88 | + - When asked, call save_answer with the file name the user gave (or a short | ||
| 89 | + name made from the subject, ending in .md). Do NOT write the tutorial | ||
| 90 | + again: save_answer saves your previous answer by itself — call it alone, | ||
| 91 | + with no text in your reply. | ||
| 92 | + - When the request is "explain X and save it", write the tutorial in your | ||
| 93 | + reply AND call save_answer in that same reply. | ||
| 94 | + - If save_answer says the file exists, tell the user and ask for another | ||
| 95 | + name. | ||
added
demos/programming-answers/AGENTS.md +2 -0 | new file mode 100644 | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +# Little Coder workspace | |
| 2 | + | |
| new file mode 100644 | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | +# Little Coder workspace | ||
| 2 | + | ||
added
demos/programming-answers/README.en.md +83 -0 | new file mode 100644 | ||
| @@ -0,0 +1,83 @@ | ||
| 1 | +# Little Coder on Mellum2.1 Thinking — settings and choices | |
| 2 | + | |
| 3 | +*[Version française](README.fr.md)* | |
| 4 | + | |
| 5 | +`agent.yaml` tunes mm for **Mellum2.1-12B-A2.5B-Thinking** (`hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M`) served by **llmman**. The case it was tuned on: "Écris-moi un tutoriel sur Nu shell" (write me a Nu shell tutorial), with or without a file name. The original configuration is kept as `agent.orig.yaml`. | |
| 6 | + | |
| 7 | +## Run | |
| 8 | + | |
| 9 | +```bash | |
| 10 | +cd wip | |
| 11 | +mm # the terminal | |
| 12 | +mm -p "Écris-moi un tutoriel sur Nu shell" | |
| 13 | +``` | |
| 14 | + | |
| 15 | +## What the model says (llmman `/api/show`) | |
| 16 | + | |
| 17 | +- 12.1 billion parameters, mixture of experts (64, 8 active), 8.07 GB in Q4_K_M. | |
| 18 | +- Native window of 131,072 tokens; llmman served 131,072 during the measurements. | |
| 19 | +- Chat template: one reasoning switch, `enable_thinking`, **on by default**. No effort variable. | |
| 20 | +- Tool calls in Hermes JSON (`<tool_call>{…}</tool_call>`). | |
| 21 | +- No sampling settings in the GGUF. | |
| 22 | + | |
| 23 | +## The settings, and why | |
| 24 | + | |
| 25 | +| Setting | Before | Now | Measured reason | | |
| 26 | +|---------|--------|-----|-----------------| | |
| 27 | +| `planTool` | `true` | `false` | 1 run out of 5 sent `plan update` about 30 times without its `status` field, until `maxTurns` (9 min 20). Writing a document needs no plan. | | |
| 28 | +| `sampling.chat_template_kwargs.reasoning_effort` | `low` | removed | The template has no such variable: with the same seed, the answer does not change in the expected direction. See "Reasoning effort" below. | | |
| 29 | +| `sampling.chat_template_kwargs.enable_thinking` | `true` | `true` | With reasoning off, 4 runs out of 6 saved the "installation" page as is instead of writing a tutorial, and one run went on for 30 minutes. | | |
| 30 | +| `sampling.max_tokens` | `32768` | `16384` | 32,768 is the whole window mm budgets for. A ~1,000-word tutorial costs 10,000–16,000 tokens in the `write_file` call (almost all of it reasoning). 16,384 was hit once out of 6, and mm's retry recovered the call. | | |
| 31 | +| `maxTurns` | `40` | `20` | A tutorial takes 4–8 page reads plus one write. 20 turns stop a looping model in a few minutes rather than 9. | | |
| 32 | +| `system` | `You are Little Coder.` | steps (below) | With one line: ~2 KB tutorials called "complete", 2 runs out of 5 written from memory, with inventions ("extensions written in Lua", `brew install nu`). | | |
| 33 | + | |
| 34 | +The other settings (`temperature: 0.2`, `parallel_tool_calls: false`, `contextWindow`, the timeouts, `agentsMd`, `context`) are unchanged. No compression was triggered during the measurements: the prompt reached about 22,000 tokens at write time. | |
| 35 | + | |
| 36 | +## The system prompt | |
| 37 | + | |
| 38 | +Three steps for a tutorial or a guide: | |
| 39 | + | |
| 40 | +1. **Research first** with `fetch_content`: the official site, then 2 to 4 documentation pages. The model is told why: its knowledge is months old and it invents commands when it writes from memory. | |
| 41 | +2. **Write the whole tutorial in ONE `write_file`**, in the language of the request, with a list of sections: installation, first steps, one section per concept with at least two examples each followed by a sentence saying what it prints, an exercise with its solution, the links read. Target: 1,000 words or more. | |
| 42 | +3. **Answer in two or three sentences**: the file name and its sections. | |
| 43 | + | |
| 44 | +Two rules: do not read the file back after writing it, and never put the tutorial in the reply instead of in `write_file`. | |
| 45 | + | |
| 46 | +## Reasoning effort | |
| 47 | + | |
| 48 | +There is none for this model. Tested directly on llmman 0.1.478 (`/v1/chat/completions`, same question, same seed, 2 runs per variant): | |
| 49 | + | |
| 50 | +| Variant | Effect | | |
| 51 | +|---------|--------| | |
| 52 | +| `enable_thinking: false` | reasoning removed (about 90 tokens instead of 500–650) | | |
| 53 | +| `reasoning_effort: low` in `chat_template_kwargs` | ignored by the template | | |
| 54 | +| `reasoning_effort: low` at the top level | ignored (same figures as the baseline) | | |
| 55 | +| `reasoning_budget: 0` or `200` | ignored (same figures as the baseline) | | |
| 56 | +| "Reasoning: low" system prompt | no effect | | |
| 57 | +| `thinking_budget_tokens: 200` | **applied, but it cuts**: the reasoning stops dead, and the model keeps thinking in its answer ("Wait need to ensure correct syntax…") | | |
| 58 | + | |
| 59 | +In the agent, `thinking_budget_tokens: 4096` dropped success from 6/6 to 4/6. Cut mid-way, the model spilled its draft into the reply, and twice the tutorial was not written to a file. So only two choices remain, reasoning on or off, and it is on. | |
| 60 | + | |
| 61 | +## Measured results (2026-10-17) | |
| 62 | + | |
| 63 | +Two requests, "Écris-moi un tutoriel sur Nu shell" and "Écris un tutoriel complet sur Nu shell dans le fichier nushell-tutorial.md", in `mm -p`, on the development machine's llmman. | |
| 64 | + | |
| 65 | +| Configuration | Runs | Finishes | Reads the docs | In French | Tutorial size | Duration | | |
| 66 | +|---------------|------|----------|----------------|-----------|---------------|----------| | |
| 67 | +| `agent.orig.yaml` | 5 | 4 | 2 | — | ~2 KB | 1 min 45 to 9 min 20 | | |
| 68 | +| no `plan`, step prompt | 6 | 6 | 6 | 3 | 2–3 KB, 5 sections | 2 min 40 to 4 min 15 | | |
| 69 | +| **+ language and section list (`agent.yaml`)** | 6 | **6** | 6 | 5 | **500–950 words, 6–11 sections** | 8 to 17 min | | |
| 70 | +| the same, softer source rule ("build the tutorial on the commands shown in the pages you fetched, and never invent a package name, a command or an option") | 6 | 4 | 6 | 4 (of the 4 written) | 385–630 words, 7–11 sections | 5 min 30 to 17 min | | |
| 71 | +| the same, `max_tokens: 24576` | 6 | 4 | 6 | 5 (of the 5 written) | 347–781 words, 5–11 sections | 10 min 30 to over 21 min | | |
| 72 | +| the same, reasoning off | 6 | 2 | — | — | the raw "installation" page 4 times | 2 to 30 min | | |
| 73 | +| the same, `thinking_budget_tokens: 4096` | 6 | 4 | 6 | — | 330–880 words | 5 min 40 to 15 min | | |
| 74 | + | |
| 75 | +"Finishes" means: a tutorial is written to a file and the run ends without an error. Six runs per configuration on a single request: an order of magnitude, not a guarantee. | |
| 76 | + | |
| 77 | +## Known limits | |
| 78 | + | |
| 79 | +- **Slowness.** Most of the time is spent in the `write_file` call, 10,000–16,000 tokens at about 30 tokens/s, because the model drafts the whole tutorial in its reasoning before writing it. | |
| 80 | +- **The rule "every command must come from a page you fetched" costs time.** In a slow run the model deliberated at length over whether `let` and `echo` appeared in the pages it had read. A softer wording was measured and **not kept**: 4 runs out of 6 instead of 6, shorter tutorials, a barely lower average duration (10 min 30 against 12 min 25). Both failures hit `max_tokens` (16,384) in the final reply, without calling `write_file`. | |
| 81 | +- **`max_tokens: 16384` is a real limit.** Hit 1 time out of 6 with v2 (in a `write_file`, which mm retries) and 2 times out of 6 with the softer rule (in the final reply, which mm does not retry). Raising it to 24,576 does not help (measured, 6 runs): the model simply reasons longer. Two runs reached the 24,576 tokens: one never wrote its file, the other ran past 20 minutes. Result: 4 out of 6 and slower runs. | |
| 82 | +- **One tutorial out of six stayed in English** despite the language rule. | |
| 83 | +- **Not measured:** the window mode (`-tui`), the editor (`-acp`), and coding requests, even though they are Little Coder's first job. The settings above were measured on writing a tutorial only. | |
| new file mode 100644 | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | +# Little Coder on Mellum2.1 Thinking — settings and choices | ||
| 2 | + | ||
| 3 | +*[Version française](README.fr.md)* | ||
| 4 | + | ||
| 5 | +`agent.yaml` tunes mm for **Mellum2.1-12B-A2.5B-Thinking** (`hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M`) served by **llmman**. The case it was tuned on: "Écris-moi un tutoriel sur Nu shell" (write me a Nu shell tutorial), with or without a file name. The original configuration is kept as `agent.orig.yaml`. | ||
| 6 | + | ||
| 7 | +## Run | ||
| 8 | + | ||
| 9 | +```bash | ||
| 10 | +cd wip | ||
| 11 | +mm # the terminal | ||
| 12 | +mm -p "Écris-moi un tutoriel sur Nu shell" | ||
| 13 | +``` | ||
| 14 | + | ||
| 15 | +## What the model says (llmman `/api/show`) | ||
| 16 | + | ||
| 17 | +- 12.1 billion parameters, mixture of experts (64, 8 active), 8.07 GB in Q4_K_M. | ||
| 18 | +- Native window of 131,072 tokens; llmman served 131,072 during the measurements. | ||
| 19 | +- Chat template: one reasoning switch, `enable_thinking`, **on by default**. No effort variable. | ||
| 20 | +- Tool calls in Hermes JSON (`<tool_call>{…}</tool_call>`). | ||
| 21 | +- No sampling settings in the GGUF. | ||
| 22 | + | ||
| 23 | +## The settings, and why | ||
| 24 | + | ||
| 25 | +| Setting | Before | Now | Measured reason | | ||
| 26 | +|---------|--------|-----|-----------------| | ||
| 27 | +| `planTool` | `true` | `false` | 1 run out of 5 sent `plan update` about 30 times without its `status` field, until `maxTurns` (9 min 20). Writing a document needs no plan. | | ||
| 28 | +| `sampling.chat_template_kwargs.reasoning_effort` | `low` | removed | The template has no such variable: with the same seed, the answer does not change in the expected direction. See "Reasoning effort" below. | | ||
| 29 | +| `sampling.chat_template_kwargs.enable_thinking` | `true` | `true` | With reasoning off, 4 runs out of 6 saved the "installation" page as is instead of writing a tutorial, and one run went on for 30 minutes. | | ||
| 30 | +| `sampling.max_tokens` | `32768` | `16384` | 32,768 is the whole window mm budgets for. A ~1,000-word tutorial costs 10,000–16,000 tokens in the `write_file` call (almost all of it reasoning). 16,384 was hit once out of 6, and mm's retry recovered the call. | | ||
| 31 | +| `maxTurns` | `40` | `20` | A tutorial takes 4–8 page reads plus one write. 20 turns stop a looping model in a few minutes rather than 9. | | ||
| 32 | +| `system` | `You are Little Coder.` | steps (below) | With one line: ~2 KB tutorials called "complete", 2 runs out of 5 written from memory, with inventions ("extensions written in Lua", `brew install nu`). | | ||
| 33 | + | ||
| 34 | +The other settings (`temperature: 0.2`, `parallel_tool_calls: false`, `contextWindow`, the timeouts, `agentsMd`, `context`) are unchanged. No compression was triggered during the measurements: the prompt reached about 22,000 tokens at write time. | ||
| 35 | + | ||
| 36 | +## The system prompt | ||
| 37 | + | ||
| 38 | +Three steps for a tutorial or a guide: | ||
| 39 | + | ||
| 40 | +1. **Research first** with `fetch_content`: the official site, then 2 to 4 documentation pages. The model is told why: its knowledge is months old and it invents commands when it writes from memory. | ||
| 41 | +2. **Write the whole tutorial in ONE `write_file`**, in the language of the request, with a list of sections: installation, first steps, one section per concept with at least two examples each followed by a sentence saying what it prints, an exercise with its solution, the links read. Target: 1,000 words or more. | ||
| 42 | +3. **Answer in two or three sentences**: the file name and its sections. | ||
| 43 | + | ||
| 44 | +Two rules: do not read the file back after writing it, and never put the tutorial in the reply instead of in `write_file`. | ||
| 45 | + | ||
| 46 | +## Reasoning effort | ||
| 47 | + | ||
| 48 | +There is none for this model. Tested directly on llmman 0.1.478 (`/v1/chat/completions`, same question, same seed, 2 runs per variant): | ||
| 49 | + | ||
| 50 | +| Variant | Effect | | ||
| 51 | +|---------|--------| | ||
| 52 | +| `enable_thinking: false` | reasoning removed (about 90 tokens instead of 500–650) | | ||
| 53 | +| `reasoning_effort: low` in `chat_template_kwargs` | ignored by the template | | ||
| 54 | +| `reasoning_effort: low` at the top level | ignored (same figures as the baseline) | | ||
| 55 | +| `reasoning_budget: 0` or `200` | ignored (same figures as the baseline) | | ||
| 56 | +| "Reasoning: low" system prompt | no effect | | ||
| 57 | +| `thinking_budget_tokens: 200` | **applied, but it cuts**: the reasoning stops dead, and the model keeps thinking in its answer ("Wait need to ensure correct syntax…") | | ||
| 58 | + | ||
| 59 | +In the agent, `thinking_budget_tokens: 4096` dropped success from 6/6 to 4/6. Cut mid-way, the model spilled its draft into the reply, and twice the tutorial was not written to a file. So only two choices remain, reasoning on or off, and it is on. | ||
| 60 | + | ||
| 61 | +## Measured results (2026-10-17) | ||
| 62 | + | ||
| 63 | +Two requests, "Écris-moi un tutoriel sur Nu shell" and "Écris un tutoriel complet sur Nu shell dans le fichier nushell-tutorial.md", in `mm -p`, on the development machine's llmman. | ||
| 64 | + | ||
| 65 | +| Configuration | Runs | Finishes | Reads the docs | In French | Tutorial size | Duration | | ||
| 66 | +|---------------|------|----------|----------------|-----------|---------------|----------| | ||
| 67 | +| `agent.orig.yaml` | 5 | 4 | 2 | — | ~2 KB | 1 min 45 to 9 min 20 | | ||
| 68 | +| no `plan`, step prompt | 6 | 6 | 6 | 3 | 2–3 KB, 5 sections | 2 min 40 to 4 min 15 | | ||
| 69 | +| **+ language and section list (`agent.yaml`)** | 6 | **6** | 6 | 5 | **500–950 words, 6–11 sections** | 8 to 17 min | | ||
| 70 | +| the same, softer source rule ("build the tutorial on the commands shown in the pages you fetched, and never invent a package name, a command or an option") | 6 | 4 | 6 | 4 (of the 4 written) | 385–630 words, 7–11 sections | 5 min 30 to 17 min | | ||
| 71 | +| the same, `max_tokens: 24576` | 6 | 4 | 6 | 5 (of the 5 written) | 347–781 words, 5–11 sections | 10 min 30 to over 21 min | | ||
| 72 | +| the same, reasoning off | 6 | 2 | — | — | the raw "installation" page 4 times | 2 to 30 min | | ||
| 73 | +| the same, `thinking_budget_tokens: 4096` | 6 | 4 | 6 | — | 330–880 words | 5 min 40 to 15 min | | ||
| 74 | + | ||
| 75 | +"Finishes" means: a tutorial is written to a file and the run ends without an error. Six runs per configuration on a single request: an order of magnitude, not a guarantee. | ||
| 76 | + | ||
| 77 | +## Known limits | ||
| 78 | + | ||
| 79 | +- **Slowness.** Most of the time is spent in the `write_file` call, 10,000–16,000 tokens at about 30 tokens/s, because the model drafts the whole tutorial in its reasoning before writing it. | ||
| 80 | +- **The rule "every command must come from a page you fetched" costs time.** In a slow run the model deliberated at length over whether `let` and `echo` appeared in the pages it had read. A softer wording was measured and **not kept**: 4 runs out of 6 instead of 6, shorter tutorials, a barely lower average duration (10 min 30 against 12 min 25). Both failures hit `max_tokens` (16,384) in the final reply, without calling `write_file`. | ||
| 81 | +- **`max_tokens: 16384` is a real limit.** Hit 1 time out of 6 with v2 (in a `write_file`, which mm retries) and 2 times out of 6 with the softer rule (in the final reply, which mm does not retry). Raising it to 24,576 does not help (measured, 6 runs): the model simply reasons longer. Two runs reached the 24,576 tokens: one never wrote its file, the other ran past 20 minutes. Result: 4 out of 6 and slower runs. | ||
| 82 | +- **One tutorial out of six stayed in English** despite the language rule. | ||
| 83 | +- **Not measured:** the window mode (`-tui`), the editor (`-acp`), and coding requests, even though they are Little Coder's first job. The settings above were measured on writing a tutorial only. | ||
added
demos/programming-answers/README.fr.md +83 -0 | new file mode 100644 | ||
| @@ -0,0 +1,83 @@ | ||
| 1 | +# Little Coder sur Mellum2.1 Thinking — réglages et choix | |
| 2 | + | |
| 3 | +*[English version](README.en.md)* | |
| 4 | + | |
| 5 | +`agent.yaml` règle mm pour **Mellum2.1-12B-A2.5B-Thinking** (`hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M`) servi par **llmman**. Le cas qui a servi à le régler : « Écris-moi un tutoriel sur Nu shell », avec ou sans nom de fichier. La configuration d'origine est conservée dans `agent.orig.yaml`. | |
| 6 | + | |
| 7 | +## Lancer | |
| 8 | + | |
| 9 | +```bash | |
| 10 | +cd wip | |
| 11 | +mm # le terminal | |
| 12 | +mm -p "Écris-moi un tutoriel sur Nu shell" | |
| 13 | +``` | |
| 14 | + | |
| 15 | +## Ce que dit le modèle (llmman `/api/show`) | |
| 16 | + | |
| 17 | +- 12,1 milliards de paramètres, mixture d'experts (64, dont 8 actifs), 8,07 Go en Q4_K_M. | |
| 18 | +- Fenêtre native de 131 072 tokens ; llmman servait 131 072 pendant les mesures. | |
| 19 | +- Template de chat : un seul interrupteur de raisonnement, `enable_thinking`, **activé par défaut**. Aucune variable d'effort. | |
| 20 | +- Appels d'outils au format Hermes JSON (`<tool_call>{…}</tool_call>`). | |
| 21 | +- Aucun réglage d'échantillonnage dans le GGUF. | |
| 22 | + | |
| 23 | +## Les réglages, et pourquoi | |
| 24 | + | |
| 25 | +| Réglage | Avant | Maintenant | Raison mesurée | | |
| 26 | +|---------|-------|------------|----------------| | |
| 27 | +| `planTool` | `true` | `false` | 1 run sur 5 a envoyé environ 30 fois `plan update` sans le champ `status`, jusqu'à `maxTurns` (9 min 20). Écrire un document n'a pas besoin de plan. | | |
| 28 | +| `sampling.chat_template_kwargs.reasoning_effort` | `low` | retiré | Le template n'a pas cette variable : à seed égale, la réponse ne change pas dans le sens attendu. Voir « L'effort de raisonnement » ci-dessous. | | |
| 29 | +| `sampling.chat_template_kwargs.enable_thinking` | `true` | `true` | Sans raisonnement, 4 runs sur 6 ont enregistré la page « installation » telle quelle au lieu d'écrire un tutoriel, et un run a tourné 30 minutes. | | |
| 30 | +| `sampling.max_tokens` | `32768` | `16384` | 32 768, c'est toute la fenêtre budgétée par mm. Un tutoriel d'environ 1 000 mots coûte 10 000 à 16 000 tokens dans l'appel `write_file` (presque tout en raisonnement). 16 384 a été atteint 1 fois sur 6, et la relance de mm a récupéré l'appel. | | |
| 31 | +| `maxTurns` | `40` | `20` | Un tutoriel prend 4 à 8 lectures de pages plus une écriture. 20 tours arrêtent un modèle qui boucle en quelques minutes plutôt qu'en 9. | | |
| 32 | +| `system` | `You are Little Coder.` | des étapes (ci-dessous) | Avec une seule ligne : des tutoriels d'environ 2 Ko dits « complets », 2 runs sur 5 écrits de mémoire avec des inventions (« extensions écrites en Lua », `brew install nu`). | | |
| 33 | + | |
| 34 | +Les autres réglages (`temperature: 0.2`, `parallel_tool_calls: false`, `contextWindow`, les délais, `agentsMd`, `context`) n'ont pas changé. Aucune compression ne s'est déclenchée pendant les mesures : le prompt atteignait environ 22 000 tokens au moment d'écrire. | |
| 35 | + | |
| 36 | +## Le prompt système | |
| 37 | + | |
| 38 | +Trois étapes pour un tutoriel ou un guide : | |
| 39 | + | |
| 40 | +1. **Se documenter d'abord** avec `fetch_content` : le site officiel, puis 2 à 4 pages de documentation. La raison est donnée au modèle : ses connaissances datent de plusieurs mois et il invente des commandes quand il écrit de mémoire. | |
| 41 | +2. **Écrire tout le tutoriel en un seul `write_file`**, dans la langue de la demande, avec une liste de sections : installation, premiers pas, une section par concept avec au moins deux exemples suivis d'une phrase qui dit ce qu'ils affichent, un exercice avec sa solution, les liens lus. Objectif : 1 000 mots ou plus. | |
| 42 | +3. **Répondre en deux ou trois phrases** : le nom du fichier et ses sections. | |
| 43 | + | |
| 44 | +Deux règles : ne pas relire le fichier après l'avoir écrit, et ne jamais mettre le tutoriel dans la réponse au lieu de `write_file`. | |
| 45 | + | |
| 46 | +## L'effort de raisonnement | |
| 47 | + | |
| 48 | +Il n'existe pas pour ce modèle. Testé directement sur llmman 0.1.478 (`/v1/chat/completions`, même question, même seed, 2 runs par variante) : | |
| 49 | + | |
| 50 | +| Variante | Effet | | |
| 51 | +|----------|-------| | |
| 52 | +| `enable_thinking: false` | raisonnement supprimé (environ 90 tokens au lieu de 500 à 650) | | |
| 53 | +| `reasoning_effort: low` dans `chat_template_kwargs` | ignoré par le template | | |
| 54 | +| `reasoning_effort: low` au premier niveau | ignoré (mêmes chiffres que la référence) | | |
| 55 | +| `reasoning_budget: 0` ou `200` | ignoré (mêmes chiffres que la référence) | | |
| 56 | +| prompt système « Reasoning: low » | aucun effet | | |
| 57 | +| `thinking_budget_tokens: 200` | **appliqué, mais il coupe** : le raisonnement s'arrête net, et le modèle continue de réfléchir dans sa réponse (« Wait need to ensure correct syntax… ») | | |
| 58 | + | |
| 59 | +Dans l'agent, `thinking_budget_tokens: 4096` a fait tomber la réussite de 6/6 à 4/6. Coupé au milieu, le modèle a déversé son brouillon dans la réponse, et deux fois le tutoriel n'a pas été écrit dans un fichier. Il ne reste donc que deux choix, raisonnement activé ou désactivé, et c'est activé. | |
| 60 | + | |
| 61 | +## Résultats mesurés (2026-10-17) | |
| 62 | + | |
| 63 | +Deux demandes, « Écris-moi un tutoriel sur Nu shell » et « Écris un tutoriel complet sur Nu shell dans le fichier nushell-tutorial.md », en `mm -p`, sur le llmman de la machine de développement. | |
| 64 | + | |
| 65 | +| Configuration | Runs | Va au bout | Lit la doc | En français | Taille du tutoriel | Durée | | |
| 66 | +|---------------|------|------------|------------|-------------|--------------------|-------| | |
| 67 | +| `agent.orig.yaml` | 5 | 4 | 2 | — | ~2 Ko | 1 min 45 à 9 min 20 | | |
| 68 | +| sans `plan`, prompt en étapes | 6 | 6 | 6 | 3 | 2 à 3 Ko, 5 sections | 2 min 40 à 4 min 15 | | |
| 69 | +| **+ langue et liste de sections (`agent.yaml`)** | 6 | **6** | 6 | 5 | **500 à 950 mots, 6 à 11 sections** | 8 à 17 min | | |
| 70 | +| la même, règle de sources assouplie (« build the tutorial on the commands shown in the pages you fetched, and never invent a package name, a command or an option ») | 6 | 4 | 6 | 4 (sur 4 écrits) | 385 à 630 mots, 7 à 11 sections | 5 min 30 à 17 min | | |
| 71 | +| la même, `max_tokens: 24576` | 6 | 4 | 6 | 5 (sur 5 écrits) | 347 à 781 mots, 5 à 11 sections | 10 min 30 à plus de 21 min | | |
| 72 | +| la même, raisonnement désactivé | 6 | 2 | — | — | 4 fois la page « installation » brute | 2 à 30 min | | |
| 73 | +| la même, `thinking_budget_tokens: 4096` | 6 | 4 | 6 | — | 330 à 880 mots | 5 min 40 à 15 min | | |
| 74 | + | |
| 75 | +« Va au bout » signifie : un tutoriel est écrit dans un fichier et le run se termine sans erreur. Ce ne sont que six runs par configuration, sur une seule demande : c'est un ordre de grandeur, pas une garantie. | |
| 76 | + | |
| 77 | +## Limites connues | |
| 78 | + | |
| 79 | +- **La lenteur.** L'essentiel du temps est passé dans l'appel `write_file`, entre 10 000 et 16 000 tokens à environ 30 tokens/s, parce que le modèle rédige tout le tutoriel dans son raisonnement avant de l'écrire. | |
| 80 | +- **La règle « chaque commande doit venir d'une page lue » coûte du temps.** Dans un run lent, le modèle a longuement délibéré pour savoir si `let` et `echo` figuraient dans les pages lues. Une version assouplie a été mesurée et **n'a pas été retenue** : 4 runs sur 6 au lieu de 6, des tutoriels plus courts, une durée moyenne à peine plus faible (10 min 30 contre 12 min 25). Les deux échecs ont atteint `max_tokens` (16 384) dans la réponse finale, sans appeler `write_file`. | |
| 81 | +- **`max_tokens: 16384` est une vraie limite.** Atteint 1 fois sur 6 avec la v2 (dans un `write_file`, que mm relance) et 2 fois sur 6 avec la règle assouplie (dans la réponse finale, que mm ne relance pas). Monter à 24 576 n'aide pas (mesuré, 6 runs) : le modèle raisonne simplement plus longtemps. Deux runs ont atteint les 24 576 tokens : l'un n'a jamais écrit son fichier, l'autre a dépassé 20 minutes. Résultat : 4 sur 6 et des runs plus lents. | |
| 82 | +- **Un tutoriel sur six est resté en anglais** malgré la règle de langue. | |
| 83 | +- **Non mesuré :** le mode fenêtre (`-tui`), l'éditeur (`-acp`), et les demandes de code, pourtant le premier métier de « Little Coder ». Les réglages ci-dessus n'ont été mesurés que sur l'écriture d'un tutoriel. | |
| new file mode 100644 | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | +# Little Coder sur Mellum2.1 Thinking — réglages et choix | ||
| 2 | + | ||
| 3 | +*[English version](README.en.md)* | ||
| 4 | + | ||
| 5 | +`agent.yaml` règle mm pour **Mellum2.1-12B-A2.5B-Thinking** (`hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M`) servi par **llmman**. Le cas qui a servi à le régler : « Écris-moi un tutoriel sur Nu shell », avec ou sans nom de fichier. La configuration d'origine est conservée dans `agent.orig.yaml`. | ||
| 6 | + | ||
| 7 | +## Lancer | ||
| 8 | + | ||
| 9 | +```bash | ||
| 10 | +cd wip | ||
| 11 | +mm # le terminal | ||
| 12 | +mm -p "Écris-moi un tutoriel sur Nu shell" | ||
| 13 | +``` | ||
| 14 | + | ||
| 15 | +## Ce que dit le modèle (llmman `/api/show`) | ||
| 16 | + | ||
| 17 | +- 12,1 milliards de paramètres, mixture d'experts (64, dont 8 actifs), 8,07 Go en Q4_K_M. | ||
| 18 | +- Fenêtre native de 131 072 tokens ; llmman servait 131 072 pendant les mesures. | ||
| 19 | +- Template de chat : un seul interrupteur de raisonnement, `enable_thinking`, **activé par défaut**. Aucune variable d'effort. | ||
| 20 | +- Appels d'outils au format Hermes JSON (`<tool_call>{…}</tool_call>`). | ||
| 21 | +- Aucun réglage d'échantillonnage dans le GGUF. | ||
| 22 | + | ||
| 23 | +## Les réglages, et pourquoi | ||
| 24 | + | ||
| 25 | +| Réglage | Avant | Maintenant | Raison mesurée | | ||
| 26 | +|---------|-------|------------|----------------| | ||
| 27 | +| `planTool` | `true` | `false` | 1 run sur 5 a envoyé environ 30 fois `plan update` sans le champ `status`, jusqu'à `maxTurns` (9 min 20). Écrire un document n'a pas besoin de plan. | | ||
| 28 | +| `sampling.chat_template_kwargs.reasoning_effort` | `low` | retiré | Le template n'a pas cette variable : à seed égale, la réponse ne change pas dans le sens attendu. Voir « L'effort de raisonnement » ci-dessous. | | ||
| 29 | +| `sampling.chat_template_kwargs.enable_thinking` | `true` | `true` | Sans raisonnement, 4 runs sur 6 ont enregistré la page « installation » telle quelle au lieu d'écrire un tutoriel, et un run a tourné 30 minutes. | | ||
| 30 | +| `sampling.max_tokens` | `32768` | `16384` | 32 768, c'est toute la fenêtre budgétée par mm. Un tutoriel d'environ 1 000 mots coûte 10 000 à 16 000 tokens dans l'appel `write_file` (presque tout en raisonnement). 16 384 a été atteint 1 fois sur 6, et la relance de mm a récupéré l'appel. | | ||
| 31 | +| `maxTurns` | `40` | `20` | Un tutoriel prend 4 à 8 lectures de pages plus une écriture. 20 tours arrêtent un modèle qui boucle en quelques minutes plutôt qu'en 9. | | ||
| 32 | +| `system` | `You are Little Coder.` | des étapes (ci-dessous) | Avec une seule ligne : des tutoriels d'environ 2 Ko dits « complets », 2 runs sur 5 écrits de mémoire avec des inventions (« extensions écrites en Lua », `brew install nu`). | | ||
| 33 | + | ||
| 34 | +Les autres réglages (`temperature: 0.2`, `parallel_tool_calls: false`, `contextWindow`, les délais, `agentsMd`, `context`) n'ont pas changé. Aucune compression ne s'est déclenchée pendant les mesures : le prompt atteignait environ 22 000 tokens au moment d'écrire. | ||
| 35 | + | ||
| 36 | +## Le prompt système | ||
| 37 | + | ||
| 38 | +Trois étapes pour un tutoriel ou un guide : | ||
| 39 | + | ||
| 40 | +1. **Se documenter d'abord** avec `fetch_content` : le site officiel, puis 2 à 4 pages de documentation. La raison est donnée au modèle : ses connaissances datent de plusieurs mois et il invente des commandes quand il écrit de mémoire. | ||
| 41 | +2. **Écrire tout le tutoriel en un seul `write_file`**, dans la langue de la demande, avec une liste de sections : installation, premiers pas, une section par concept avec au moins deux exemples suivis d'une phrase qui dit ce qu'ils affichent, un exercice avec sa solution, les liens lus. Objectif : 1 000 mots ou plus. | ||
| 42 | +3. **Répondre en deux ou trois phrases** : le nom du fichier et ses sections. | ||
| 43 | + | ||
| 44 | +Deux règles : ne pas relire le fichier après l'avoir écrit, et ne jamais mettre le tutoriel dans la réponse au lieu de `write_file`. | ||
| 45 | + | ||
| 46 | +## L'effort de raisonnement | ||
| 47 | + | ||
| 48 | +Il n'existe pas pour ce modèle. Testé directement sur llmman 0.1.478 (`/v1/chat/completions`, même question, même seed, 2 runs par variante) : | ||
| 49 | + | ||
| 50 | +| Variante | Effet | | ||
| 51 | +|----------|-------| | ||
| 52 | +| `enable_thinking: false` | raisonnement supprimé (environ 90 tokens au lieu de 500 à 650) | | ||
| 53 | +| `reasoning_effort: low` dans `chat_template_kwargs` | ignoré par le template | | ||
| 54 | +| `reasoning_effort: low` au premier niveau | ignoré (mêmes chiffres que la référence) | | ||
| 55 | +| `reasoning_budget: 0` ou `200` | ignoré (mêmes chiffres que la référence) | | ||
| 56 | +| prompt système « Reasoning: low » | aucun effet | | ||
| 57 | +| `thinking_budget_tokens: 200` | **appliqué, mais il coupe** : le raisonnement s'arrête net, et le modèle continue de réfléchir dans sa réponse (« Wait need to ensure correct syntax… ») | | ||
| 58 | + | ||
| 59 | +Dans l'agent, `thinking_budget_tokens: 4096` a fait tomber la réussite de 6/6 à 4/6. Coupé au milieu, le modèle a déversé son brouillon dans la réponse, et deux fois le tutoriel n'a pas été écrit dans un fichier. Il ne reste donc que deux choix, raisonnement activé ou désactivé, et c'est activé. | ||
| 60 | + | ||
| 61 | +## Résultats mesurés (2026-10-17) | ||
| 62 | + | ||
| 63 | +Deux demandes, « Écris-moi un tutoriel sur Nu shell » et « Écris un tutoriel complet sur Nu shell dans le fichier nushell-tutorial.md », en `mm -p`, sur le llmman de la machine de développement. | ||
| 64 | + | ||
| 65 | +| Configuration | Runs | Va au bout | Lit la doc | En français | Taille du tutoriel | Durée | | ||
| 66 | +|---------------|------|------------|------------|-------------|--------------------|-------| | ||
| 67 | +| `agent.orig.yaml` | 5 | 4 | 2 | — | ~2 Ko | 1 min 45 à 9 min 20 | | ||
| 68 | +| sans `plan`, prompt en étapes | 6 | 6 | 6 | 3 | 2 à 3 Ko, 5 sections | 2 min 40 à 4 min 15 | | ||
| 69 | +| **+ langue et liste de sections (`agent.yaml`)** | 6 | **6** | 6 | 5 | **500 à 950 mots, 6 à 11 sections** | 8 à 17 min | | ||
| 70 | +| la même, règle de sources assouplie (« build the tutorial on the commands shown in the pages you fetched, and never invent a package name, a command or an option ») | 6 | 4 | 6 | 4 (sur 4 écrits) | 385 à 630 mots, 7 à 11 sections | 5 min 30 à 17 min | | ||
| 71 | +| la même, `max_tokens: 24576` | 6 | 4 | 6 | 5 (sur 5 écrits) | 347 à 781 mots, 5 à 11 sections | 10 min 30 à plus de 21 min | | ||
| 72 | +| la même, raisonnement désactivé | 6 | 2 | — | — | 4 fois la page « installation » brute | 2 à 30 min | | ||
| 73 | +| la même, `thinking_budget_tokens: 4096` | 6 | 4 | 6 | — | 330 à 880 mots | 5 min 40 à 15 min | | ||
| 74 | + | ||
| 75 | +« Va au bout » signifie : un tutoriel est écrit dans un fichier et le run se termine sans erreur. Ce ne sont que six runs par configuration, sur une seule demande : c'est un ordre de grandeur, pas une garantie. | ||
| 76 | + | ||
| 77 | +## Limites connues | ||
| 78 | + | ||
| 79 | +- **La lenteur.** L'essentiel du temps est passé dans l'appel `write_file`, entre 10 000 et 16 000 tokens à environ 30 tokens/s, parce que le modèle rédige tout le tutoriel dans son raisonnement avant de l'écrire. | ||
| 80 | +- **La règle « chaque commande doit venir d'une page lue » coûte du temps.** Dans un run lent, le modèle a longuement délibéré pour savoir si `let` et `echo` figuraient dans les pages lues. Une version assouplie a été mesurée et **n'a pas été retenue** : 4 runs sur 6 au lieu de 6, des tutoriels plus courts, une durée moyenne à peine plus faible (10 min 30 contre 12 min 25). Les deux échecs ont atteint `max_tokens` (16 384) dans la réponse finale, sans appeler `write_file`. | ||
| 81 | +- **`max_tokens: 16384` est une vraie limite.** Atteint 1 fois sur 6 avec la v2 (dans un `write_file`, que mm relance) et 2 fois sur 6 avec la règle assouplie (dans la réponse finale, que mm ne relance pas). Monter à 24 576 n'aide pas (mesuré, 6 runs) : le modèle raisonne simplement plus longtemps. Deux runs ont atteint les 24 576 tokens : l'un n'a jamais écrit son fichier, l'autre a dépassé 20 minutes. Résultat : 4 sur 6 et des runs plus lents. | ||
| 82 | +- **Un tutoriel sur six est resté en anglais** malgré la règle de langue. | ||
| 83 | +- **Non mesuré :** le mode fenêtre (`-tui`), l'éditeur (`-acp`), et les demandes de code, pourtant le premier métier de « Little Coder ». Les réglages ci-dessus n'ont été mesurés que sur l'écriture d'un tutoriel. | ||
added
demos/programming-answers/agent.orig.yaml +105 -0 | new file mode 100644 | ||
| @@ -0,0 +1,105 @@ | ||
| 1 | +provider: llmman | |
| 2 | +model: hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M | |
| 3 | + | |
| 4 | +baseUrl: http://127.0.0.1:17434/v1 | |
| 5 | +fallback: http://host.docker.internal:17434/v1 | |
| 6 | + | |
| 7 | +# The window mm budgets for, whatever llmman serves: compression starts at | |
| 8 | +# `context.threshold` % of it. 32k matches LLMMAN_CONTEXT_LENGTH above; if | |
| 9 | +# llmman serves more, mm simply compresses earlier — never set it higher than | |
| 10 | +# what llmman serves. | |
| 11 | +contextWindow: 32768 | |
| 12 | + | |
| 13 | +# ─── Generation ────────────────────────────────────────────────────────────── | |
| 14 | + | |
| 15 | +sampling: | |
| 16 | + # Code wants precision. Not 0.0: greedy decoding is what sends a small | |
| 17 | + # model into repetition loops on long tool calls. | |
| 18 | + temperature: 0.2 | |
| 19 | + top_p: 0.9 | |
| 20 | + | |
| 21 | + # ONE reply = one tool call. A whole file in one write_file (main.go, | |
| 22 | + # a template) is 1–3k tokens; 8192 leaves margin without letting a runaway | |
| 23 | + # reply fill the window. | |
| 24 | + max_tokens: 32768 | |
| 25 | + | |
| 26 | + # One file, one command at a time. | |
| 27 | + parallel_tool_calls: false | |
| 28 | + | |
| 29 | + # chat_template_kwargs: | |
| 30 | + # enable_thinking: false | |
| 31 | + | |
| 32 | + chat_template_kwargs: | |
| 33 | + enable_thinking: true | |
| 34 | + reasoning_effort: low | |
| 35 | + | |
| 36 | +showThinking: true | |
| 37 | + | |
| 38 | +# ─── Timeouts ──────────────────────────────────────────────────────────────── | |
| 39 | + | |
| 40 | +# Loading ~7 GB on the first question, then prompts of 10–20k tokens. | |
| 41 | +firstTokenTimeout: 3m | |
| 42 | + | |
| 43 | +# A first `go build` / `go test` downloads nothing (standard library only) | |
| 44 | +# but compiles net/http: under a minute on an M-series Air. | |
| 45 | +bashTimeout: 2m | |
| 46 | + | |
| 47 | +# Counts while a bash command runs: at least bashTimeout. | |
| 48 | +watchdogTimeout: 2m | |
| 49 | + | |
| 50 | +# ─── Agent loop ────────────────────────────────────────────────────────────── | |
| 51 | + | |
| 52 | +# A sample app is ~6 files + build + test + run + check + fixes. | |
| 53 | +maxTurns: 40 | |
| 54 | + | |
| 55 | +# go vet / go test failures are short; 8000 characters keep a whole report | |
| 56 | +# without filling a 32k window with logs. | |
| 57 | +maxOutput: 8000 | |
| 58 | + | |
| 59 | +previewLines: 15 | |
| 60 | +displayCommands: true | |
| 61 | + | |
| 62 | +# ─── Tools ─────────────────────────────────────────────────────────────────── | |
| 63 | + | |
| 64 | +# true: no question before each command or file write — generating an app is | |
| 65 | +# a dozen of them. Set false (or AGENT_YOLO=false) to approve each one. | |
| 66 | +yolo: true | |
| 67 | + | |
| 68 | +bashTool: true | |
| 69 | +editTools: true | |
| 70 | + | |
| 71 | +planTool: true | |
| 72 | +fetchContentTool: true | |
| 73 | + | |
| 74 | +#skillsDir: skills | |
| 75 | + | |
| 76 | +# The rules of this workspace (Go version, stack, layout), appended to the | |
| 77 | +# system prompt. Kept short: it is sent with every request. | |
| 78 | +agentsMd: | |
| 79 | + enabled: true | |
| 80 | + path: AGENTS.md | |
| 81 | + # Guard rail: a file grown past ~1,000 tokens is skipped, not sent. | |
| 82 | + maxChars: 4000 | |
| 83 | + | |
| 84 | +sessions: | |
| 85 | + enabled: true | |
| 86 | + dir: .mm/sessions | |
| 87 | + | |
| 88 | +context: | |
| 89 | + enabled: true | |
| 90 | + # 60 % of 32k ≈ 19.6k tokens: compress before the prompt gets long enough | |
| 91 | + # for the model to lose track of the files it wrote. | |
| 92 | + threshold: 60 | |
| 93 | + maxMessages: 60 | |
| 94 | + keepLastTurns: 2 | |
| 95 | + summaryMaxTokens: 1200 | |
| 96 | + showStats: true | |
| 97 | + | |
| 98 | +# ─── Identity ──────────────────────────────────────────────────────────────── | |
| 99 | +acp: | |
| 100 | + name: little-coder | |
| 101 | + title: 🤓 Little Coder | |
| 102 | + | |
| 103 | +system: | | |
| 104 | + You are Little Coder. | |
| 105 | + | |
| new file mode 100644 | |||
| @@ -0,0 +1,105 @@ | |||
| 1 | +provider: llmman | ||
| 2 | +model: hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M | ||
| 3 | + | ||
| 4 | +baseUrl: http://127.0.0.1:17434/v1 | ||
| 5 | +fallback: http://host.docker.internal:17434/v1 | ||
| 6 | + | ||
| 7 | +# The window mm budgets for, whatever llmman serves: compression starts at | ||
| 8 | +# `context.threshold` % of it. 32k matches LLMMAN_CONTEXT_LENGTH above; if | ||
| 9 | +# llmman serves more, mm simply compresses earlier — never set it higher than | ||
| 10 | +# what llmman serves. | ||
| 11 | +contextWindow: 32768 | ||
| 12 | + | ||
| 13 | +# ─── Generation ────────────────────────────────────────────────────────────── | ||
| 14 | + | ||
| 15 | +sampling: | ||
| 16 | + # Code wants precision. Not 0.0: greedy decoding is what sends a small | ||
| 17 | + # model into repetition loops on long tool calls. | ||
| 18 | + temperature: 0.2 | ||
| 19 | + top_p: 0.9 | ||
| 20 | + | ||
| 21 | + # ONE reply = one tool call. A whole file in one write_file (main.go, | ||
| 22 | + # a template) is 1–3k tokens; 8192 leaves margin without letting a runaway | ||
| 23 | + # reply fill the window. | ||
| 24 | + max_tokens: 32768 | ||
| 25 | + | ||
| 26 | + # One file, one command at a time. | ||
| 27 | + parallel_tool_calls: false | ||
| 28 | + | ||
| 29 | + # chat_template_kwargs: | ||
| 30 | + # enable_thinking: false | ||
| 31 | + | ||
| 32 | + chat_template_kwargs: | ||
| 33 | + enable_thinking: true | ||
| 34 | + reasoning_effort: low | ||
| 35 | + | ||
| 36 | +showThinking: true | ||
| 37 | + | ||
| 38 | +# ─── Timeouts ──────────────────────────────────────────────────────────────── | ||
| 39 | + | ||
| 40 | +# Loading ~7 GB on the first question, then prompts of 10–20k tokens. | ||
| 41 | +firstTokenTimeout: 3m | ||
| 42 | + | ||
| 43 | +# A first `go build` / `go test` downloads nothing (standard library only) | ||
| 44 | +# but compiles net/http: under a minute on an M-series Air. | ||
| 45 | +bashTimeout: 2m | ||
| 46 | + | ||
| 47 | +# Counts while a bash command runs: at least bashTimeout. | ||
| 48 | +watchdogTimeout: 2m | ||
| 49 | + | ||
| 50 | +# ─── Agent loop ────────────────────────────────────────────────────────────── | ||
| 51 | + | ||
| 52 | +# A sample app is ~6 files + build + test + run + check + fixes. | ||
| 53 | +maxTurns: 40 | ||
| 54 | + | ||
| 55 | +# go vet / go test failures are short; 8000 characters keep a whole report | ||
| 56 | +# without filling a 32k window with logs. | ||
| 57 | +maxOutput: 8000 | ||
| 58 | + | ||
| 59 | +previewLines: 15 | ||
| 60 | +displayCommands: true | ||
| 61 | + | ||
| 62 | +# ─── Tools ─────────────────────────────────────────────────────────────────── | ||
| 63 | + | ||
| 64 | +# true: no question before each command or file write — generating an app is | ||
| 65 | +# a dozen of them. Set false (or AGENT_YOLO=false) to approve each one. | ||
| 66 | +yolo: true | ||
| 67 | + | ||
| 68 | +bashTool: true | ||
| 69 | +editTools: true | ||
| 70 | + | ||
| 71 | +planTool: true | ||
| 72 | +fetchContentTool: true | ||
| 73 | + | ||
| 74 | +#skillsDir: skills | ||
| 75 | + | ||
| 76 | +# The rules of this workspace (Go version, stack, layout), appended to the | ||
| 77 | +# system prompt. Kept short: it is sent with every request. | ||
| 78 | +agentsMd: | ||
| 79 | + enabled: true | ||
| 80 | + path: AGENTS.md | ||
| 81 | + # Guard rail: a file grown past ~1,000 tokens is skipped, not sent. | ||
| 82 | + maxChars: 4000 | ||
| 83 | + | ||
| 84 | +sessions: | ||
| 85 | + enabled: true | ||
| 86 | + dir: .mm/sessions | ||
| 87 | + | ||
| 88 | +context: | ||
| 89 | + enabled: true | ||
| 90 | + # 60 % of 32k ≈ 19.6k tokens: compress before the prompt gets long enough | ||
| 91 | + # for the model to lose track of the files it wrote. | ||
| 92 | + threshold: 60 | ||
| 93 | + maxMessages: 60 | ||
| 94 | + keepLastTurns: 2 | ||
| 95 | + summaryMaxTokens: 1200 | ||
| 96 | + showStats: true | ||
| 97 | + | ||
| 98 | +# ─── Identity ──────────────────────────────────────────────────────────────── | ||
| 99 | +acp: | ||
| 100 | + name: little-coder | ||
| 101 | + title: 🤓 Little Coder | ||
| 102 | + | ||
| 103 | +system: | | ||
| 104 | + You are Little Coder. | ||
| 105 | + | ||
added
demos/programming-answers/agent.yaml +142 -0 | new file mode 100644 | ||
| @@ -0,0 +1,142 @@ | ||
| 1 | +provider: llmman | |
| 2 | +model: hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M | |
| 3 | + | |
| 4 | +baseUrl: http://127.0.0.1:17434/v1 | |
| 5 | +fallback: http://host.docker.internal:17434/v1 | |
| 6 | + | |
| 7 | +# The window mm budgets for, whatever llmman serves: compression starts at | |
| 8 | +# `context.threshold` % of it. 32k matches LLMMAN_CONTEXT_LENGTH above; if | |
| 9 | +# llmman serves more, mm simply compresses earlier — never set it higher than | |
| 10 | +# what llmman serves. | |
| 11 | +contextWindow: 32768 | |
| 12 | + | |
| 13 | +# ─── Generation ────────────────────────────────────────────────────────────── | |
| 14 | + | |
| 15 | +sampling: | |
| 16 | + # Code wants precision. Not 0.0: greedy decoding is what sends a small | |
| 17 | + # model into repetition loops on long tool calls. | |
| 18 | + temperature: 0.2 | |
| 19 | + top_p: 0.9 | |
| 20 | + | |
| 21 | + # ONE reply = one tool call, reasoning included. Measured 2026-10-17: the | |
| 22 | + # write_file of a ~1k-word tutorial costs 10–16k tokens, almost all of it | |
| 23 | + # reasoning (the model drafts the whole text in its thinking first); 16384 | |
| 24 | + # was hit once out of six and mm's retry recovered. 24576 measured worse | |
| 25 | + # (4/6): the model just reasons longer, twice up to the new limit. | |
| 26 | + max_tokens: 16384 | |
| 27 | + | |
| 28 | + # One file, one command at a time. | |
| 29 | + parallel_tool_calls: false | |
| 30 | + | |
| 31 | + # chat_template_kwargs: | |
| 32 | + # enable_thinking: false | |
| 33 | + | |
| 34 | + # Mellum2.1's template has ONE switch, enable_thinking — no effort level | |
| 35 | + # (reasoning_effort is ignored, measured). Off: the agent saved a fetched | |
| 36 | + # page instead of writing the tutorial (4/6) and once ran 30 min. Capping | |
| 37 | + # it with thinking_budget_tokens: 4096 cut the reasoning mid-way and the | |
| 38 | + # draft spilled into the reply, the tutorial missing its file (2/6). | |
| 39 | + chat_template_kwargs: | |
| 40 | + enable_thinking: true | |
| 41 | + | |
| 42 | +showThinking: true | |
| 43 | + | |
| 44 | +# ─── Timeouts ──────────────────────────────────────────────────────────────── | |
| 45 | + | |
| 46 | +# Loading ~7 GB on the first question, then prompts of 10–20k tokens. | |
| 47 | +firstTokenTimeout: 3m | |
| 48 | + | |
| 49 | +# A first `go build` / `go test` downloads nothing (standard library only) | |
| 50 | +# but compiles net/http: under a minute on an M-series Air. | |
| 51 | +bashTimeout: 2m | |
| 52 | + | |
| 53 | +# Counts while a bash command runs: at least bashTimeout. | |
| 54 | +watchdogTimeout: 2m | |
| 55 | + | |
| 56 | +# ─── Agent loop ────────────────────────────────────────────────────────────── | |
| 57 | + | |
| 58 | +# A tutorial takes 4–8 fetches + 1 write. 20 also stops a looping model | |
| 59 | +# in minutes instead of the 9 minutes 40 turns took (plan update loop). | |
| 60 | +maxTurns: 20 | |
| 61 | + | |
| 62 | +# go vet / go test failures are short; 8000 characters keep a whole report | |
| 63 | +# without filling a 32k window with logs. | |
| 64 | +maxOutput: 8000 | |
| 65 | + | |
| 66 | +previewLines: 15 | |
| 67 | +displayCommands: true | |
| 68 | + | |
| 69 | +# ─── Tools ─────────────────────────────────────────────────────────────────── | |
| 70 | + | |
| 71 | +# true: no question before each command or file write — generating an app is | |
| 72 | +# a dozen of them. Set false (or AGENT_YOLO=false) to approve each one. | |
| 73 | +yolo: true | |
| 74 | + | |
| 75 | +bashTool: true | |
| 76 | +editTools: true | |
| 77 | + | |
| 78 | +# Off: Mellum2.1 looped on `plan update` without its `status` field until | |
| 79 | +# maxTurns (1 run out of 5); writing a document needs no plan. | |
| 80 | +planTool: false | |
| 81 | +fetchContentTool: true | |
| 82 | + | |
| 83 | +#skillsDir: skills | |
| 84 | + | |
| 85 | +# The rules of this workspace (Go version, stack, layout), appended to the | |
| 86 | +# system prompt. Kept short: it is sent with every request. | |
| 87 | +agentsMd: | |
| 88 | + enabled: true | |
| 89 | + path: AGENTS.md | |
| 90 | + # Guard rail: a file grown past ~1,000 tokens is skipped, not sent. | |
| 91 | + maxChars: 4000 | |
| 92 | + | |
| 93 | +sessions: | |
| 94 | + enabled: true | |
| 95 | + dir: .mm/sessions | |
| 96 | + | |
| 97 | +context: | |
| 98 | + enabled: true | |
| 99 | + # 60 % of 32k ≈ 19.6k tokens: compress before the prompt gets long enough | |
| 100 | + # for the model to lose track of the files it wrote. | |
| 101 | + threshold: 60 | |
| 102 | + maxMessages: 60 | |
| 103 | + keepLastTurns: 2 | |
| 104 | + summaryMaxTokens: 1200 | |
| 105 | + showStats: true | |
| 106 | + | |
| 107 | +# ─── Identity ──────────────────────────────────────────────────────────────── | |
| 108 | +acp: | |
| 109 | + name: little-coder | |
| 110 | + title: 🤓 Little Coder | |
| 111 | + | |
| 112 | +system: | | |
| 113 | + You are Little Coder, a coding and documentation assistant. Answer in the | |
| 114 | + user's language. | |
| 115 | + | |
| 116 | + WRITING A TUTORIAL OR A GUIDE | |
| 117 | + 1. Research first, with fetch_content: the project's official site, then | |
| 118 | + 2 to 4 pages of its documentation (getting started, the main concepts). | |
| 119 | + Your knowledge is months old and you invent commands when you write | |
| 120 | + from memory; every command in the tutorial must come from a page you | |
| 121 | + fetched. | |
| 122 | + 2. Write the whole tutorial in ONE write_file call (a .md file named after | |
| 123 | + the subject when the user gives no name), in the language of the | |
| 124 | + user's request: a request in French gets a tutorial in French. | |
| 125 | + A complete tutorial has these sections: | |
| 126 | + - Installation (the commands from the fetched page) | |
| 127 | + - First steps | |
| 128 | + - one section per core concept found in the pages (for a shell: | |
| 129 | + structured data and tables, pipelines, filtering and sorting, | |
| 130 | + opening JSON/CSV files, variables, custom commands, configuration), | |
| 131 | + each with at least two examples and, after each example, one | |
| 132 | + sentence saying what it prints | |
| 133 | + - a practical exercise, with its solution | |
| 134 | + - the links you fetched | |
| 135 | + Aim for 1,000 words or more. | |
| 136 | + 3. Answer in two or three sentences: the file name and its sections. | |
| 137 | + Do not paste the tutorial again. | |
| 138 | + | |
| 139 | + RULES | |
| 140 | + - Do not read a file back after writing it: write_file reports success. | |
| 141 | + - Never put the tutorial in your reply instead of in write_file. | |
| 142 | + | |
| new file mode 100644 | |||
| @@ -0,0 +1,142 @@ | |||
| 1 | +provider: llmman | ||
| 2 | +model: hf.co/JetBrains/Mellum2.1-12B-A2.5B-Thinking-GGUF:Q4_K_M | ||
| 3 | + | ||
| 4 | +baseUrl: http://127.0.0.1:17434/v1 | ||
| 5 | +fallback: http://host.docker.internal:17434/v1 | ||
| 6 | + | ||
| 7 | +# The window mm budgets for, whatever llmman serves: compression starts at | ||
| 8 | +# `context.threshold` % of it. 32k matches LLMMAN_CONTEXT_LENGTH above; if | ||
| 9 | +# llmman serves more, mm simply compresses earlier — never set it higher than | ||
| 10 | +# what llmman serves. | ||
| 11 | +contextWindow: 32768 | ||
| 12 | + | ||
| 13 | +# ─── Generation ────────────────────────────────────────────────────────────── | ||
| 14 | + | ||
| 15 | +sampling: | ||
| 16 | + # Code wants precision. Not 0.0: greedy decoding is what sends a small | ||
| 17 | + # model into repetition loops on long tool calls. | ||
| 18 | + temperature: 0.2 | ||
| 19 | + top_p: 0.9 | ||
| 20 | + | ||
| 21 | + # ONE reply = one tool call, reasoning included. Measured 2026-10-17: the | ||
| 22 | + # write_file of a ~1k-word tutorial costs 10–16k tokens, almost all of it | ||
| 23 | + # reasoning (the model drafts the whole text in its thinking first); 16384 | ||
| 24 | + # was hit once out of six and mm's retry recovered. 24576 measured worse | ||
| 25 | + # (4/6): the model just reasons longer, twice up to the new limit. | ||
| 26 | + max_tokens: 16384 | ||
| 27 | + | ||
| 28 | + # One file, one command at a time. | ||
| 29 | + parallel_tool_calls: false | ||
| 30 | + | ||
| 31 | + # chat_template_kwargs: | ||
| 32 | + # enable_thinking: false | ||
| 33 | + | ||
| 34 | + # Mellum2.1's template has ONE switch, enable_thinking — no effort level | ||
| 35 | + # (reasoning_effort is ignored, measured). Off: the agent saved a fetched | ||
| 36 | + # page instead of writing the tutorial (4/6) and once ran 30 min. Capping | ||
| 37 | + # it with thinking_budget_tokens: 4096 cut the reasoning mid-way and the | ||
| 38 | + # draft spilled into the reply, the tutorial missing its file (2/6). | ||
| 39 | + chat_template_kwargs: | ||
| 40 | + enable_thinking: true | ||
| 41 | + | ||
| 42 | +showThinking: true | ||
| 43 | + | ||
| 44 | +# ─── Timeouts ──────────────────────────────────────────────────────────────── | ||
| 45 | + | ||
| 46 | +# Loading ~7 GB on the first question, then prompts of 10–20k tokens. | ||
| 47 | +firstTokenTimeout: 3m | ||
| 48 | + | ||
| 49 | +# A first `go build` / `go test` downloads nothing (standard library only) | ||
| 50 | +# but compiles net/http: under a minute on an M-series Air. | ||
| 51 | +bashTimeout: 2m | ||
| 52 | + | ||
| 53 | +# Counts while a bash command runs: at least bashTimeout. | ||
| 54 | +watchdogTimeout: 2m | ||
| 55 | + | ||
| 56 | +# ─── Agent loop ────────────────────────────────────────────────────────────── | ||
| 57 | + | ||
| 58 | +# A tutorial takes 4–8 fetches + 1 write. 20 also stops a looping model | ||
| 59 | +# in minutes instead of the 9 minutes 40 turns took (plan update loop). | ||
| 60 | +maxTurns: 20 | ||
| 61 | + | ||
| 62 | +# go vet / go test failures are short; 8000 characters keep a whole report | ||
| 63 | +# without filling a 32k window with logs. | ||
| 64 | +maxOutput: 8000 | ||
| 65 | + | ||
| 66 | +previewLines: 15 | ||
| 67 | +displayCommands: true | ||
| 68 | + | ||
| 69 | +# ─── Tools ─────────────────────────────────────────────────────────────────── | ||
| 70 | + | ||
| 71 | +# true: no question before each command or file write — generating an app is | ||
| 72 | +# a dozen of them. Set false (or AGENT_YOLO=false) to approve each one. | ||
| 73 | +yolo: true | ||
| 74 | + | ||
| 75 | +bashTool: true | ||
| 76 | +editTools: true | ||
| 77 | + | ||
| 78 | +# Off: Mellum2.1 looped on `plan update` without its `status` field until | ||
| 79 | +# maxTurns (1 run out of 5); writing a document needs no plan. | ||
| 80 | +planTool: false | ||
| 81 | +fetchContentTool: true | ||
| 82 | + | ||
| 83 | +#skillsDir: skills | ||
| 84 | + | ||
| 85 | +# The rules of this workspace (Go version, stack, layout), appended to the | ||
| 86 | +# system prompt. Kept short: it is sent with every request. | ||
| 87 | +agentsMd: | ||
| 88 | + enabled: true | ||
| 89 | + path: AGENTS.md | ||
| 90 | + # Guard rail: a file grown past ~1,000 tokens is skipped, not sent. | ||
| 91 | + maxChars: 4000 | ||
| 92 | + | ||
| 93 | +sessions: | ||
| 94 | + enabled: true | ||
| 95 | + dir: .mm/sessions | ||
| 96 | + | ||
| 97 | +context: | ||
| 98 | + enabled: true | ||
| 99 | + # 60 % of 32k ≈ 19.6k tokens: compress before the prompt gets long enough | ||
| 100 | + # for the model to lose track of the files it wrote. | ||
| 101 | + threshold: 60 | ||
| 102 | + maxMessages: 60 | ||
| 103 | + keepLastTurns: 2 | ||
| 104 | + summaryMaxTokens: 1200 | ||
| 105 | + showStats: true | ||
| 106 | + | ||
| 107 | +# ─── Identity ──────────────────────────────────────────────────────────────── | ||
| 108 | +acp: | ||
| 109 | + name: little-coder | ||
| 110 | + title: 🤓 Little Coder | ||
| 111 | + | ||
| 112 | +system: | | ||
| 113 | + You are Little Coder, a coding and documentation assistant. Answer in the | ||
| 114 | + user's language. | ||
| 115 | + | ||
| 116 | + WRITING A TUTORIAL OR A GUIDE | ||
| 117 | + 1. Research first, with fetch_content: the project's official site, then | ||
| 118 | + 2 to 4 pages of its documentation (getting started, the main concepts). | ||
| 119 | + Your knowledge is months old and you invent commands when you write | ||
| 120 | + from memory; every command in the tutorial must come from a page you | ||
| 121 | + fetched. | ||
| 122 | + 2. Write the whole tutorial in ONE write_file call (a .md file named after | ||
| 123 | + the subject when the user gives no name), in the language of the | ||
| 124 | + user's request: a request in French gets a tutorial in French. | ||
| 125 | + A complete tutorial has these sections: | ||
| 126 | + - Installation (the commands from the fetched page) | ||
| 127 | + - First steps | ||
| 128 | + - one section per core concept found in the pages (for a shell: | ||
| 129 | + structured data and tables, pipelines, filtering and sorting, | ||
| 130 | + opening JSON/CSV files, variables, custom commands, configuration), | ||
| 131 | + each with at least two examples and, after each example, one | ||
| 132 | + sentence saying what it prints | ||
| 133 | + - a practical exercise, with its solution | ||
| 134 | + - the links you fetched | ||
| 135 | + Aim for 1,000 words or more. | ||
| 136 | + 3. Answer in two or three sentences: the file name and its sections. | ||
| 137 | + Do not paste the tutorial again. | ||
| 138 | + | ||
| 139 | + RULES | ||
| 140 | + - Do not read a file back after writing it: write_file reports success. | ||
| 141 | + - Never put the tutorial in your reply instead of in write_file. | ||
| 142 | + | ||
modified
docs/diagrams/packages.drawio +1 -1 | @@ -1,3 +1,3 @@ | ||
| 1 | 1 | <mxfile host="app.diagrams.net" modified="2026-10-06T00: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)</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="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</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="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_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></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)</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</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="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_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></root></mxGraphModel></diagram></mxfile> | |
| \ No newline at end of file | ||
| @@ -1,3 +1,3 @@ | |||
| 1 | <mxfile host="app.diagrams.net" modified="2026-10-06T00: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-06T00: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)</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="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</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="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_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></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)</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</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="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_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></root></mxGraphModel></diagram></mxfile> | ||
| \ No newline at end of file | \ No newline at end of file | ||
modified
docs/en/README.md +2 -1 | @@ -26,6 +26,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | ||
| 26 | 26 | - [Run mini-me in a Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — the `mini-me` kit from [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit), the `go-toolchain` and `cli-tools` mixins under `sbx/`, window mode and editor |
| 27 | 27 | - [Add a skill](how-to/add-a-skill.md) |
| 28 | 28 | - [Declare a custom tool](how-to/declare-a-custom-tool.md) |
| 29 | +- [Save an answer to a file](how-to/save-an-answer.md) | |
| 29 | 30 | - [Measure whether the model picks the right skill](how-to/measure-skill-routing.md) — the routing eval: run it, read its table, use it as a before-and-after |
| 30 | 31 | - [Keep a long session inside the context window](how-to/manage-the-context-window.md) |
| 31 | 32 | - [Start a new session without restarting](how-to/start-a-new-session.md) |
| @@ -49,7 +50,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | ||
| 49 | 50 | - [Command line](reference/cli.md) — flags, environment variables, start-up banner |
| 50 | 51 | - [Slash commands](reference/slash-commands.md) — `/quit`, `/abort`, `/compact`, `/new`, `/sessions`, `/resume`, `/help`, keyboard shortcuts, window keys |
| 51 | 52 | - [Configuration file](reference/configuration.md) — every YAML key, its default and its validation rule |
| 52 | -- [Built-in tools](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user`, declared tools | |
| 53 | +- [Built-in tools](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user`, `save_answer`, declared tools | |
| 53 | 54 | - [Skill file format](reference/skill-format.md) |
| 54 | 55 | - [Hooks](reference/hooks.md) — the six events, the JSON a hook reads and writes, `matcher`, `timeout`, `onError`, validation errors |
| 55 | 56 | - [Agent Client Protocol surface](reference/acp.md) |
| @@ -26,6 +26,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | |||
| 26 | - [Run mini-me in a Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — the `mini-me` kit from [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit), the `go-toolchain` and `cli-tools` mixins under `sbx/`, window mode and editor | 26 | - [Run mini-me in a Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — the `mini-me` kit from [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit), the `go-toolchain` and `cli-tools` mixins under `sbx/`, window mode and editor |
| 27 | - [Add a skill](how-to/add-a-skill.md) | 27 | - [Add a skill](how-to/add-a-skill.md) |
| 28 | - [Declare a custom tool](how-to/declare-a-custom-tool.md) | 28 | - [Declare a custom tool](how-to/declare-a-custom-tool.md) |
| 29 | +- [Save an answer to a file](how-to/save-an-answer.md) | ||
| 29 | - [Measure whether the model picks the right skill](how-to/measure-skill-routing.md) — the routing eval: run it, read its table, use it as a before-and-after | 30 | - [Measure whether the model picks the right skill](how-to/measure-skill-routing.md) — the routing eval: run it, read its table, use it as a before-and-after |
| 30 | - [Keep a long session inside the context window](how-to/manage-the-context-window.md) | 31 | - [Keep a long session inside the context window](how-to/manage-the-context-window.md) |
| 31 | - [Start a new session without restarting](how-to/start-a-new-session.md) | 32 | - [Start a new session without restarting](how-to/start-a-new-session.md) |
| @@ -49,7 +50,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr) | |||
| 49 | - [Command line](reference/cli.md) — flags, environment variables, start-up banner | 50 | - [Command line](reference/cli.md) — flags, environment variables, start-up banner |
| 50 | - [Slash commands](reference/slash-commands.md) — `/quit`, `/abort`, `/compact`, `/new`, `/sessions`, `/resume`, `/help`, keyboard shortcuts, window keys | 51 | - [Slash commands](reference/slash-commands.md) — `/quit`, `/abort`, `/compact`, `/new`, `/sessions`, `/resume`, `/help`, keyboard shortcuts, window keys |
| 51 | - [Configuration file](reference/configuration.md) — every YAML key, its default and its validation rule | 52 | - [Configuration file](reference/configuration.md) — every YAML key, its default and its validation rule |
| 52 | -- [Built-in tools](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user`, declared tools | 53 | +- [Built-in tools](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user`, `save_answer`, declared tools |
| 53 | - [Skill file format](reference/skill-format.md) | 54 | - [Skill file format](reference/skill-format.md) |
| 54 | - [Hooks](reference/hooks.md) — the six events, the JSON a hook reads and writes, `matcher`, `timeout`, `onError`, validation errors | 55 | - [Hooks](reference/hooks.md) — the six events, the JSON a hook reads and writes, `matcher`, `timeout`, `onError`, validation errors |
| 55 | - [Agent Client Protocol surface](reference/acp.md) | 56 | - [Agent Client Protocol surface](reference/acp.md) |
modified
docs/en/how-to/manage-plans-and-tasks.md +10 -0 | @@ -392,6 +392,16 @@ If the agent isn't creating plans for your multi-step requests: | ||
| 392 | 392 | |
| 393 | 393 | 3. **Check the skill is loaded** - The agent should automatically load the `plan-manager` skill when it detects a plan-like request |
| 394 | 394 | |
| 395 | +### The Agent Keeps Calling `plan update` | |
| 396 | + | |
| 397 | +`update` changes the status of one task; it cannot change a plan's tasks. A model that sends an `update` without a valid `status` — often with a `tasks` list, trying to edit the plan — gets an answer that quotes its call back, names the missing field, says that `tasks` is only read by `create`, and gives a valid call: | |
| 398 | + | |
| 399 | +``` | |
| 400 | +[error: plan update changes the status of ONE task, and "status" is missing. You sent: {…}. "status" must be one of: open, in_progress, done, failed. "tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again. A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}] | |
| 401 | +``` | |
| 402 | + | |
| 403 | +If a small model still repeats the call, set `planTool: false` for an agent whose job does not need plans (writing a document, answering questions): the model then works without them. | |
| 404 | + | |
| 395 | 405 | ### Plans Disappearing |
| 396 | 406 | |
| 397 | 407 | In v1, plans are stored in memory and will be lost when: |
| @@ -392,6 +392,16 @@ If the agent isn't creating plans for your multi-step requests: | |||
| 392 | 392 | ||
| 393 | 3. **Check the skill is loaded** - The agent should automatically load the `plan-manager` skill when it detects a plan-like request | 393 | 3. **Check the skill is loaded** - The agent should automatically load the `plan-manager` skill when it detects a plan-like request |
| 394 | 394 | ||
| 395 | +### The Agent Keeps Calling `plan update` | ||
| 396 | + | ||
| 397 | +`update` changes the status of one task; it cannot change a plan's tasks. A model that sends an `update` without a valid `status` — often with a `tasks` list, trying to edit the plan — gets an answer that quotes its call back, names the missing field, says that `tasks` is only read by `create`, and gives a valid call: | ||
| 398 | + | ||
| 399 | +``` | ||
| 400 | +[error: plan update changes the status of ONE task, and "status" is missing. You sent: {…}. "status" must be one of: open, in_progress, done, failed. "tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again. A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}] | ||
| 401 | +``` | ||
| 402 | + | ||
| 403 | +If a small model still repeats the call, set `planTool: false` for an agent whose job does not need plans (writing a document, answering questions): the model then works without them. | ||
| 404 | + | ||
| 395 | ### Plans Disappearing | 405 | ### Plans Disappearing |
| 396 | 406 | ||
| 397 | In v1, plans are stored in memory and will be lost when: | 407 | In v1, plans are stored in memory and will be lost when: |
added
docs/en/how-to/save-an-answer.md +59 -0 | new file mode 100644 | ||
| @@ -0,0 +1,59 @@ | ||
| 1 | +# How to save an answer to a file | |
| 2 | + | |
| 3 | +This guide shows how to keep an answer of the agent — a mini tutorial, an explanation, a snippet — in a Markdown file, without the model writing it a second time. It assumes you use mm in the terminal or the window mode (`-tui`). | |
| 4 | + | |
| 5 | +There are two ways. `/save` is a command you type: nothing is asked of the model. `save_answer` is a tool the model calls when you ask in your own words. | |
| 6 | + | |
| 7 | +## Save with the command | |
| 8 | + | |
| 9 | +1. Ask your question and wait for the answer. | |
| 10 | + | |
| 11 | +2. Type: | |
| 12 | + | |
| 13 | + ``` | |
| 14 | + /save nushell-first-script.md | |
| 15 | + ``` | |
| 16 | + | |
| 17 | + mm writes the last answer as it is and prints: | |
| 18 | + | |
| 19 | + ``` | |
| 20 | + 💾 saved the last answer to nushell-first-script.md (2418 bytes) | |
| 21 | + ``` | |
| 22 | + | |
| 23 | +3. To save an earlier answer, add its rank from the end: `/save rust-app.md 2` saves the answer before the last one. | |
| 24 | + | |
| 25 | +## Save by asking the agent | |
| 26 | + | |
| 27 | +1. Turn the tool on in the agent file: | |
| 28 | + | |
| 29 | + ```yaml | |
| 30 | + saveAnswerTool: true | |
| 31 | + ``` | |
| 32 | + | |
| 33 | +2. Ask in your own words, after the answer or in the same request: | |
| 34 | + | |
| 35 | + ``` | |
| 36 | + save it in nushell-first-script.md | |
| 37 | + ``` | |
| 38 | + | |
| 39 | + ``` | |
| 40 | + teach me pattern matching in Kotlin and save it in kotlin-match.md | |
| 41 | + ``` | |
| 42 | + | |
| 43 | + The model only sends the file name; mm writes the text the model already gave. The terminal shows: | |
| 44 | + | |
| 45 | + ``` | |
| 46 | + 💾 save_answer: nushell-first-script.md (the previous answer) | |
| 47 | + ``` | |
| 48 | + | |
| 49 | +## Variants | |
| 50 | + | |
| 51 | +- **The file already exists.** Neither `/save` nor `save_answer` overwrites it: they say so and write nothing. Give another name, or remove the file first. | |
| 52 | +- **A directory that does not exist yet.** `/save notes/rust/app.md` creates `notes/rust/`. | |
| 53 | +- **You want each save confirmed.** With `yolo: false`, `save_answer` asks before writing, like `write_file`. `/save` never asks: you typed it. | |
| 54 | +- **In an editor (ACP).** `/save` is not offered there; `save_answer` works, and writes in the session's directory. | |
| 55 | + | |
| 56 | +## See also | |
| 57 | + | |
| 58 | +- The command: [slash commands](../reference/slash-commands.md) | |
| 59 | +- The tool: [`save_answer`](../reference/tools.md#save_answer) and [`saveAnswerTool`](../reference/configuration.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | +# How to save an answer to a file | ||
| 2 | + | ||
| 3 | +This guide shows how to keep an answer of the agent — a mini tutorial, an explanation, a snippet — in a Markdown file, without the model writing it a second time. It assumes you use mm in the terminal or the window mode (`-tui`). | ||
| 4 | + | ||
| 5 | +There are two ways. `/save` is a command you type: nothing is asked of the model. `save_answer` is a tool the model calls when you ask in your own words. | ||
| 6 | + | ||
| 7 | +## Save with the command | ||
| 8 | + | ||
| 9 | +1. Ask your question and wait for the answer. | ||
| 10 | + | ||
| 11 | +2. Type: | ||
| 12 | + | ||
| 13 | + ``` | ||
| 14 | + /save nushell-first-script.md | ||
| 15 | + ``` | ||
| 16 | + | ||
| 17 | + mm writes the last answer as it is and prints: | ||
| 18 | + | ||
| 19 | + ``` | ||
| 20 | + 💾 saved the last answer to nushell-first-script.md (2418 bytes) | ||
| 21 | + ``` | ||
| 22 | + | ||
| 23 | +3. To save an earlier answer, add its rank from the end: `/save rust-app.md 2` saves the answer before the last one. | ||
| 24 | + | ||
| 25 | +## Save by asking the agent | ||
| 26 | + | ||
| 27 | +1. Turn the tool on in the agent file: | ||
| 28 | + | ||
| 29 | + ```yaml | ||
| 30 | + saveAnswerTool: true | ||
| 31 | + ``` | ||
| 32 | + | ||
| 33 | +2. Ask in your own words, after the answer or in the same request: | ||
| 34 | + | ||
| 35 | + ``` | ||
| 36 | + save it in nushell-first-script.md | ||
| 37 | + ``` | ||
| 38 | + | ||
| 39 | + ``` | ||
| 40 | + teach me pattern matching in Kotlin and save it in kotlin-match.md | ||
| 41 | + ``` | ||
| 42 | + | ||
| 43 | + The model only sends the file name; mm writes the text the model already gave. The terminal shows: | ||
| 44 | + | ||
| 45 | + ``` | ||
| 46 | + 💾 save_answer: nushell-first-script.md (the previous answer) | ||
| 47 | + ``` | ||
| 48 | + | ||
| 49 | +## Variants | ||
| 50 | + | ||
| 51 | +- **The file already exists.** Neither `/save` nor `save_answer` overwrites it: they say so and write nothing. Give another name, or remove the file first. | ||
| 52 | +- **A directory that does not exist yet.** `/save notes/rust/app.md` creates `notes/rust/`. | ||
| 53 | +- **You want each save confirmed.** With `yolo: false`, `save_answer` asks before writing, like `write_file`. `/save` never asks: you typed it. | ||
| 54 | +- **In an editor (ACP).** `/save` is not offered there; `save_answer` works, and writes in the session's directory. | ||
| 55 | + | ||
| 56 | +## See also | ||
| 57 | + | ||
| 58 | +- The command: [slash commands](../reference/slash-commands.md) | ||
| 59 | +- The tool: [`save_answer`](../reference/tools.md#save_answer) and [`saveAnswerTool`](../reference/configuration.md) | ||
modified
docs/en/reference/configuration.md +2 -1 | @@ -24,6 +24,7 @@ | ||
| 24 | 24 | | `editTools` | bool | `true` | Declare `read_file`, `write_file`, `edit_file`. `false` leaves `bash` and `read_skill` only. | |
| 25 | 25 | | `fetchContentTool` | bool | `false` | Declare `fetch_content`, which fetches web pages as Markdown. Off by default: it is the only built-in tool that reaches the network. | |
| 26 | 26 | | `askUserTool` | bool | `true` | Declare `ask_user`, through which the model asks the user questions (pick one, pick several, type a text, yes or no) and waits for the answers. `false` removes it: the model cannot ask. Never declared beside a `structuredOutput`. Overridden by `AGENT_ASK_USER`; for an agent asked by another one, also by the asker's `mmConfigs.<name>.askUser`. See [tools](tools.md#ask_user). | |
| 27 | +| `saveAnswerTool` | bool | `false` | Declare `save_answer`, which writes an answer the model already gave to a new file: the model sends a file name, not the text again. Off by default. The `/save` command exists either way. See [tools](tools.md#save_answer). | | |
| 27 | 28 | | `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. A key the OpenAI API does not declare is sent as it is, at the top level of the request — for example `chat_template_kwargs: {enable_thinking: false}`, which tells a llama.cpp server (`dmr`, `llamacpp`, `llmman`) not to reason; `reasoning_effort: none` does the same on Ollama. `messages`, `tools`, `tool_choice` (and the legacy `functions`, `function_call`) are ignored: Genkit builds them from the conversation. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). | |
| 28 | 29 | | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. | |
| 29 | 30 | | `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama sends no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). | |
| @@ -126,7 +127,7 @@ The shipped `agent.lsp.yaml` is `agent.dmr.yaml` with this block on (gopls activ | ||
| 126 | 127 | | Key | Type | Default | Description | |
| 127 | 128 | |-----|------|---------|-------------| |
| 128 | 129 | | `tools` | list | `[]` | One entry per declared tool. | |
| 129 | -| `tools[].name` | string | — | What the model calls. Required. Starts with a letter; letters, digits, `_`, `-`; at most 64 characters. Unique. Not the name of a built-in tool (`ask_agent`, `ask_user`, `bash`, `edit_file`, `fetch_content`, `lsp`, `plan`, `read_file`, `read_skill`, `write_file`), even one this agent turns off. | | |
| 130 | +| `tools[].name` | string | — | What the model calls. Required. Starts with a letter; letters, digits, `_`, `-`; at most 64 characters. Unique. Not the name of a built-in tool (`ask_agent`, `ask_user`, `bash`, `edit_file`, `fetch_content`, `lsp`, `plan`, `read_file`, `read_skill`, `save_answer`, `write_file`), even one this agent turns off. | | |
| 130 | 131 | | `tools[].description` | string | — | What the tool does and when to use it, shown to the model. Required. A line `Required parameters: …` is appended. | |
| 131 | 132 | | `tools[].parameters` | map name → parameter | `{}` | The arguments, in the order written. Names follow the `name` rules. | |
| 132 | 133 | | `tools[].parameters.<p>.type` | string | `string` | `string`, `number`, `integer` or `boolean`. | |
| @@ -24,6 +24,7 @@ | |||
| 24 | | `editTools` | bool | `true` | Declare `read_file`, `write_file`, `edit_file`. `false` leaves `bash` and `read_skill` only. | | 24 | | `editTools` | bool | `true` | Declare `read_file`, `write_file`, `edit_file`. `false` leaves `bash` and `read_skill` only. | |
| 25 | | `fetchContentTool` | bool | `false` | Declare `fetch_content`, which fetches web pages as Markdown. Off by default: it is the only built-in tool that reaches the network. | | 25 | | `fetchContentTool` | bool | `false` | Declare `fetch_content`, which fetches web pages as Markdown. Off by default: it is the only built-in tool that reaches the network. | |
| 26 | | `askUserTool` | bool | `true` | Declare `ask_user`, through which the model asks the user questions (pick one, pick several, type a text, yes or no) and waits for the answers. `false` removes it: the model cannot ask. Never declared beside a `structuredOutput`. Overridden by `AGENT_ASK_USER`; for an agent asked by another one, also by the asker's `mmConfigs.<name>.askUser`. See [tools](tools.md#ask_user). | | 26 | | `askUserTool` | bool | `true` | Declare `ask_user`, through which the model asks the user questions (pick one, pick several, type a text, yes or no) and waits for the answers. `false` removes it: the model cannot ask. Never declared beside a `structuredOutput`. Overridden by `AGENT_ASK_USER`; for an agent asked by another one, also by the asker's `mmConfigs.<name>.askUser`. See [tools](tools.md#ask_user). | |
| 27 | +| `saveAnswerTool` | bool | `false` | Declare `save_answer`, which writes an answer the model already gave to a new file: the model sends a file name, not the text again. Off by default. The `/save` command exists either way. See [tools](tools.md#save_answer). | | ||
| 27 | | `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. A key the OpenAI API does not declare is sent as it is, at the top level of the request — for example `chat_template_kwargs: {enable_thinking: false}`, which tells a llama.cpp server (`dmr`, `llamacpp`, `llmman`) not to reason; `reasoning_effort: none` does the same on Ollama. `messages`, `tools`, `tool_choice` (and the legacy `functions`, `function_call`) are ignored: Genkit builds them from the conversation. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). | | 28 | | `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. A key the OpenAI API does not declare is sent as it is, at the top level of the request — for example `chat_template_kwargs: {enable_thinking: false}`, which tells a llama.cpp server (`dmr`, `llamacpp`, `llmman`) not to reason; `reasoning_effort: none` does the same on Ollama. `messages`, `tools`, `tool_choice` (and the legacy `functions`, `function_call`) are ignored: Genkit builds them from the conversation. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). | |
| 28 | | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. | | 29 | | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. | |
| 29 | | `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama sends no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). | | 30 | | `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama sends no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). | |
| @@ -126,7 +127,7 @@ The shipped `agent.lsp.yaml` is `agent.dmr.yaml` with this block on (gopls activ | |||
| 126 | | Key | Type | Default | Description | | 127 | | Key | Type | Default | Description | |
| 127 | |-----|------|---------|-------------| | 128 | |-----|------|---------|-------------| |
| 128 | | `tools` | list | `[]` | One entry per declared tool. | | 129 | | `tools` | list | `[]` | One entry per declared tool. | |
| 129 | -| `tools[].name` | string | — | What the model calls. Required. Starts with a letter; letters, digits, `_`, `-`; at most 64 characters. Unique. Not the name of a built-in tool (`ask_agent`, `ask_user`, `bash`, `edit_file`, `fetch_content`, `lsp`, `plan`, `read_file`, `read_skill`, `write_file`), even one this agent turns off. | | 130 | +| `tools[].name` | string | — | What the model calls. Required. Starts with a letter; letters, digits, `_`, `-`; at most 64 characters. Unique. Not the name of a built-in tool (`ask_agent`, `ask_user`, `bash`, `edit_file`, `fetch_content`, `lsp`, `plan`, `read_file`, `read_skill`, `save_answer`, `write_file`), even one this agent turns off. | |
| 130 | | `tools[].description` | string | — | What the tool does and when to use it, shown to the model. Required. A line `Required parameters: …` is appended. | | 131 | | `tools[].description` | string | — | What the tool does and when to use it, shown to the model. Required. A line `Required parameters: …` is appended. | |
| 131 | | `tools[].parameters` | map name → parameter | `{}` | The arguments, in the order written. Names follow the `name` rules. | | 132 | | `tools[].parameters` | map name → parameter | `{}` | The arguments, in the order written. Names follow the `name` rules. | |
| 132 | | `tools[].parameters.<p>.type` | string | `string` | `string`, `number`, `integer` or `boolean`. | | 133 | | `tools[].parameters.<p>.type` | string | `string` | `string`, `number`, `integer` or `boolean`. | |
modified
docs/en/reference/slash-commands.md +1 -0 | @@ -14,6 +14,7 @@ | ||
| 14 | 14 | | `/resume [id]` | Continues a stored session: the most recently updated one other than the current, skipping empty ones, or the named id. Prints `↩️ resumed <id> — N message(s), started with "<first question>"` (plus ` · ⚠️ N error(s)` when the session recorded errors); resets the loop detector and the server's token count; later turns are saved to that session. Refused with a `⚠️` reason when sessions are off, when there is no earlier session, when the id is unknown or is the current session. | Sent to the model as a new message. | |
| 15 | 15 | | `/help` | Prints the commands of this front end, one per line with their description, aligned. | Sent to the model as a new message. | |
| 16 | 16 | | `/copy [n\|all]` | Copies the last answer (`n`: the n-th from the end) to the clipboard through `pbcopy`/`wl-copy`/`xclip`/`xsel` or, failing that, OSC 52 when stdout is a terminal, and prints `📋 copied N chars (<road>)` or `⚠️ …`. `/copy all` is window-mode only: here it says so. | Sent to the model as a new message. | |
| 17 | +| `/save <file> [n]` | Writes the last answer (`n`: the n-th from the end) as it is to a new file, creating the missing directories, and prints `💾 saved the last answer to <file> (N bytes)`. Never overwrites: an existing file gives `⚠️ <file> already exists — nothing was written; choose another name`. A file name with spaces is not supported. The model is not called. | Sent to the model as a new message. | | |
| 17 | 18 | |
| 18 | 19 | With `sessions.enabled`, `/new` leaves the current session's file as it is; the next question opens a new file. An empty line at the prompt is ignored. |
| 19 | 20 | |
| @@ -14,6 +14,7 @@ | |||
| 14 | | `/resume [id]` | Continues a stored session: the most recently updated one other than the current, skipping empty ones, or the named id. Prints `↩️ resumed <id> — N message(s), started with "<first question>"` (plus ` · ⚠️ N error(s)` when the session recorded errors); resets the loop detector and the server's token count; later turns are saved to that session. Refused with a `⚠️` reason when sessions are off, when there is no earlier session, when the id is unknown or is the current session. | Sent to the model as a new message. | | 14 | | `/resume [id]` | Continues a stored session: the most recently updated one other than the current, skipping empty ones, or the named id. Prints `↩️ resumed <id> — N message(s), started with "<first question>"` (plus ` · ⚠️ N error(s)` when the session recorded errors); resets the loop detector and the server's token count; later turns are saved to that session. Refused with a `⚠️` reason when sessions are off, when there is no earlier session, when the id is unknown or is the current session. | Sent to the model as a new message. | |
| 15 | | `/help` | Prints the commands of this front end, one per line with their description, aligned. | Sent to the model as a new message. | | 15 | | `/help` | Prints the commands of this front end, one per line with their description, aligned. | Sent to the model as a new message. | |
| 16 | | `/copy [n\|all]` | Copies the last answer (`n`: the n-th from the end) to the clipboard through `pbcopy`/`wl-copy`/`xclip`/`xsel` or, failing that, OSC 52 when stdout is a terminal, and prints `📋 copied N chars (<road>)` or `⚠️ …`. `/copy all` is window-mode only: here it says so. | Sent to the model as a new message. | | 16 | | `/copy [n\|all]` | Copies the last answer (`n`: the n-th from the end) to the clipboard through `pbcopy`/`wl-copy`/`xclip`/`xsel` or, failing that, OSC 52 when stdout is a terminal, and prints `📋 copied N chars (<road>)` or `⚠️ …`. `/copy all` is window-mode only: here it says so. | Sent to the model as a new message. | |
| 17 | +| `/save <file> [n]` | Writes the last answer (`n`: the n-th from the end) as it is to a new file, creating the missing directories, and prints `💾 saved the last answer to <file> (N bytes)`. Never overwrites: an existing file gives `⚠️ <file> already exists — nothing was written; choose another name`. A file name with spaces is not supported. The model is not called. | Sent to the model as a new message. | | ||
| 17 | 18 | ||
| 18 | With `sessions.enabled`, `/new` leaves the current session's file as it is; the next question opens a new file. An empty line at the prompt is ignored. | 19 | With `sessions.enabled`, `/new` leaves the current session's file as it is; the next question opens a new file. An empty line at the prompt is ignored. |
| 19 | 20 | ||
modified
docs/en/reference/tools.md +20 -0 | @@ -248,6 +248,26 @@ The declared schema describes a question's fields in words, without `properties` | ||
| 248 | 248 | | Result | Answered: `The user answered:` then one `- <question> → <answer>` line per question (several options joined by `, `). Declined: `[the user declined to answer: do not ask the same thing again; go on without it or stop]`. Cancelled: `[the question was cancelled before the user answered]`. Nobody to ask: `[the user cannot be asked in this mode: make a reasonable assumption, say which, and go on]`. Anything but an answer ends the call as failed in the window and the editor. | |
| 249 | 249 | | Errors | `[ask_user: no question — send questions: [{question, type, options}]]`; `[ask_user: N questions — ask at most 4 at a time]`; `[ask_user: question N has no text]`; `[ask_user: question N is a choice question — give it at least two options]` (or `multi`); `[ask_user: question N has type "x" — use choice, multi, text or confirm]`. | |
| 250 | 250 | |
| 251 | +## `save_answer` | |
| 252 | + | |
| 253 | +Declared when `saveAnswerTool: true` (default `false`). | |
| 254 | + | |
| 255 | +| Field | Type | Description | | |
| 256 | +|-------|------|-------------| | |
| 257 | +| `path` | string | The file to create, relative to the working directory. It must not exist yet. | | |
| 258 | + | |
| 259 | +| Aspect | Behaviour | | |
| 260 | +|--------|-----------| | |
| 261 | +| Text written | The text of the model message that made the call, when it has any; otherwise the last answer of the conversation as it stood when the turn started. Written as it is, with a trailing newline; the model does not generate it again. | | |
| 262 | +| Existing file | Never overwritten: `[save_answer: <path> already exists — nothing was written. Pick another name, or ask the user]`. | | |
| 263 | +| No answer yet | `[save_answer: there is no answer to save yet — write the answer in your reply first, then call save_answer with the file name]`; nothing is asked. | | |
| 264 | +| Directories | Missing parent directories are created. | | |
| 265 | +| Result | `saved <this answer\|the previous answer> to <path> (N bytes), starting with: <first line>`. | | |
| 266 | +| Working directory | The process directory in terminal mode; the session `cwd` in ACP mode and the window. | | |
| 267 | +| Permission | Requested when `yolo: false`, as for `write_file`; a refusal returns `[save_answer rejected by the user]`. | | |
| 268 | +| Terminal display | `💾 save_answer: <path> (this answer\|the previous answer)`, then the result. | | |
| 269 | +| ACP and window | Tool call of kind `edit`, with the file's absolute path. | | |
| 270 | + | |
| 251 | 271 | ## Declared tools |
| 252 | 272 | |
| 253 | 273 | Declared from the `tools:` block of the agent file, one tool per entry (see [configuration](configuration.md#tools-keys)). Each has the entry's `name`; its fields are the entry's `parameters`. |
| @@ -248,6 +248,26 @@ The declared schema describes a question's fields in words, without `properties` | |||
| 248 | | Result | Answered: `The user answered:` then one `- <question> → <answer>` line per question (several options joined by `, `). Declined: `[the user declined to answer: do not ask the same thing again; go on without it or stop]`. Cancelled: `[the question was cancelled before the user answered]`. Nobody to ask: `[the user cannot be asked in this mode: make a reasonable assumption, say which, and go on]`. Anything but an answer ends the call as failed in the window and the editor. | | 248 | | Result | Answered: `The user answered:` then one `- <question> → <answer>` line per question (several options joined by `, `). Declined: `[the user declined to answer: do not ask the same thing again; go on without it or stop]`. Cancelled: `[the question was cancelled before the user answered]`. Nobody to ask: `[the user cannot be asked in this mode: make a reasonable assumption, say which, and go on]`. Anything but an answer ends the call as failed in the window and the editor. | |
| 249 | | Errors | `[ask_user: no question — send questions: [{question, type, options}]]`; `[ask_user: N questions — ask at most 4 at a time]`; `[ask_user: question N has no text]`; `[ask_user: question N is a choice question — give it at least two options]` (or `multi`); `[ask_user: question N has type "x" — use choice, multi, text or confirm]`. | | 249 | | Errors | `[ask_user: no question — send questions: [{question, type, options}]]`; `[ask_user: N questions — ask at most 4 at a time]`; `[ask_user: question N has no text]`; `[ask_user: question N is a choice question — give it at least two options]` (or `multi`); `[ask_user: question N has type "x" — use choice, multi, text or confirm]`. | |
| 250 | 250 | ||
| 251 | +## `save_answer` | ||
| 252 | + | ||
| 253 | +Declared when `saveAnswerTool: true` (default `false`). | ||
| 254 | + | ||
| 255 | +| Field | Type | Description | | ||
| 256 | +|-------|------|-------------| | ||
| 257 | +| `path` | string | The file to create, relative to the working directory. It must not exist yet. | | ||
| 258 | + | ||
| 259 | +| Aspect | Behaviour | | ||
| 260 | +|--------|-----------| | ||
| 261 | +| Text written | The text of the model message that made the call, when it has any; otherwise the last answer of the conversation as it stood when the turn started. Written as it is, with a trailing newline; the model does not generate it again. | | ||
| 262 | +| Existing file | Never overwritten: `[save_answer: <path> already exists — nothing was written. Pick another name, or ask the user]`. | | ||
| 263 | +| No answer yet | `[save_answer: there is no answer to save yet — write the answer in your reply first, then call save_answer with the file name]`; nothing is asked. | | ||
| 264 | +| Directories | Missing parent directories are created. | | ||
| 265 | +| Result | `saved <this answer\|the previous answer> to <path> (N bytes), starting with: <first line>`. | | ||
| 266 | +| Working directory | The process directory in terminal mode; the session `cwd` in ACP mode and the window. | | ||
| 267 | +| Permission | Requested when `yolo: false`, as for `write_file`; a refusal returns `[save_answer rejected by the user]`. | | ||
| 268 | +| Terminal display | `💾 save_answer: <path> (this answer\|the previous answer)`, then the result. | | ||
| 269 | +| ACP and window | Tool call of kind `edit`, with the file's absolute path. | | ||
| 270 | + | ||
| 251 | ## Declared tools | 271 | ## Declared tools |
| 252 | 272 | ||
| 253 | Declared from the `tools:` block of the agent file, one tool per entry (see [configuration](configuration.md#tools-keys)). Each has the entry's `name`; its fields are the entry's `parameters`. | 273 | Declared from the `tools:` block of the agent file, one tool per entry (see [configuration](configuration.md#tools-keys)). Each has the entry's `name`; its fields are the entry's `parameters`. |
modified
docs/fr/README.md +2 -1 | @@ -26,6 +26,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | ||
| 26 | 26 | - [Faire tourner mini-me dans un Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — le kit `mini-me` de [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit), les mixins `go-toolchain` et `cli-tools` sous `sbx/`, mode fenêtre et éditeur |
| 27 | 27 | - [Ajouter un skill](how-to/add-a-skill.md) |
| 28 | 28 | - [Déclarer un outil personnalisé](how-to/declare-a-custom-tool.md) |
| 29 | +- [Sauvegarder une réponse dans un fichier](how-to/save-an-answer.md) | |
| 29 | 30 | - [Mesurer si le modèle choisit le bon skill](how-to/measure-skill-routing.md) — l'eval de routage : le lancer, lire son tableau, s'en servir en avant/après |
| 30 | 31 | - [Garder une longue session dans la fenêtre de contexte](how-to/manage-the-context-window.md) |
| 31 | 32 | - [Démarrer une nouvelle session sans relancer](how-to/start-a-new-session.md) |
| @@ -49,7 +50,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | ||
| 49 | 50 | - [Ligne de commande](reference/cli.md) — options, variables d'environnement, bannière de démarrage |
| 50 | 51 | - [Commandes slash](reference/slash-commands.md) — `/quit`, `/abort`, `/compact`, `/new`, `/sessions`, `/resume`, `/help`, raccourcis clavier, touches de la fenêtre |
| 51 | 52 | - [Fichier de configuration](reference/configuration.md) — chaque clé YAML, sa valeur par défaut et sa règle de validation |
| 52 | -- [Outils intégrés](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user`, outils déclarés | |
| 53 | +- [Outils intégrés](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user`, `save_answer`, outils déclarés | |
| 53 | 54 | - [Format d'un fichier de skill](reference/skill-format.md) |
| 54 | 55 | - [Les hooks](reference/hooks.md) — les six événements, le JSON qu'un hook lit et écrit, `matcher`, `timeout`, `onError`, erreurs de validation |
| 55 | 56 | - [Surface Agent Client Protocol](reference/acp.md) |
| @@ -26,6 +26,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | |||
| 26 | - [Faire tourner mini-me dans un Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — le kit `mini-me` de [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit), les mixins `go-toolchain` et `cli-tools` sous `sbx/`, mode fenêtre et éditeur | 26 | - [Faire tourner mini-me dans un Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — le kit `mini-me` de [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit), les mixins `go-toolchain` et `cli-tools` sous `sbx/`, mode fenêtre et éditeur |
| 27 | - [Ajouter un skill](how-to/add-a-skill.md) | 27 | - [Ajouter un skill](how-to/add-a-skill.md) |
| 28 | - [Déclarer un outil personnalisé](how-to/declare-a-custom-tool.md) | 28 | - [Déclarer un outil personnalisé](how-to/declare-a-custom-tool.md) |
| 29 | +- [Sauvegarder une réponse dans un fichier](how-to/save-an-answer.md) | ||
| 29 | - [Mesurer si le modèle choisit le bon skill](how-to/measure-skill-routing.md) — l'eval de routage : le lancer, lire son tableau, s'en servir en avant/après | 30 | - [Mesurer si le modèle choisit le bon skill](how-to/measure-skill-routing.md) — l'eval de routage : le lancer, lire son tableau, s'en servir en avant/après |
| 30 | - [Garder une longue session dans la fenêtre de contexte](how-to/manage-the-context-window.md) | 31 | - [Garder une longue session dans la fenêtre de contexte](how-to/manage-the-context-window.md) |
| 31 | - [Démarrer une nouvelle session sans relancer](how-to/start-a-new-session.md) | 32 | - [Démarrer une nouvelle session sans relancer](how-to/start-a-new-session.md) |
| @@ -49,7 +50,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https: | |||
| 49 | - [Ligne de commande](reference/cli.md) — options, variables d'environnement, bannière de démarrage | 50 | - [Ligne de commande](reference/cli.md) — options, variables d'environnement, bannière de démarrage |
| 50 | - [Commandes slash](reference/slash-commands.md) — `/quit`, `/abort`, `/compact`, `/new`, `/sessions`, `/resume`, `/help`, raccourcis clavier, touches de la fenêtre | 51 | - [Commandes slash](reference/slash-commands.md) — `/quit`, `/abort`, `/compact`, `/new`, `/sessions`, `/resume`, `/help`, raccourcis clavier, touches de la fenêtre |
| 51 | - [Fichier de configuration](reference/configuration.md) — chaque clé YAML, sa valeur par défaut et sa règle de validation | 52 | - [Fichier de configuration](reference/configuration.md) — chaque clé YAML, sa valeur par défaut et sa règle de validation |
| 52 | -- [Outils intégrés](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user`, outils déclarés | 53 | +- [Outils intégrés](reference/tools.md) — `bash`, `read_skill`, `read_file`, `write_file`, `edit_file`, `lsp`, `fetch_content`, `ask_agent`, `ask_user`, `save_answer`, outils déclarés |
| 53 | - [Format d'un fichier de skill](reference/skill-format.md) | 54 | - [Format d'un fichier de skill](reference/skill-format.md) |
| 54 | - [Les hooks](reference/hooks.md) — les six événements, le JSON qu'un hook lit et écrit, `matcher`, `timeout`, `onError`, erreurs de validation | 55 | - [Les hooks](reference/hooks.md) — les six événements, le JSON qu'un hook lit et écrit, `matcher`, `timeout`, `onError`, erreurs de validation |
| 55 | - [Surface Agent Client Protocol](reference/acp.md) | 56 | - [Surface Agent Client Protocol](reference/acp.md) |
modified
docs/fr/how-to/manage-plans-and-tasks.md +10 -0 | @@ -392,6 +392,16 @@ Si l'agent ne crée pas de plans pour vos demandes multi-étapes : | ||
| 392 | 392 | |
| 393 | 393 | 3. **Vérifiez que la compétence est chargée** - L'agent devrait automatiquement charger la compétence `plan-manager` lorsqu'il détecte une demande de type plan |
| 394 | 394 | |
| 395 | +### L'agent appelle `plan update` en boucle | |
| 396 | + | |
| 397 | +`update` change l'état d'une tâche ; il ne peut pas modifier les tâches d'un plan. Un modèle qui envoie un `update` sans `status` valide — souvent avec une liste `tasks`, pour essayer de modifier le plan — reçoit une réponse qui cite son appel, nomme le champ manquant, précise que `tasks` n'est lu que par `create`, et donne un appel valide : | |
| 398 | + | |
| 399 | +``` | |
| 400 | +[error: plan update changes the status of ONE task, and "status" is missing. You sent: {…}. "status" must be one of: open, in_progress, done, failed. "tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again. A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}] | |
| 401 | +``` | |
| 402 | + | |
| 403 | +Si un petit modèle répète quand même l'appel, mettez `planTool: false` pour un agent dont le métier n'a pas besoin de plans (écrire un document, répondre à des questions) : le modèle travaille alors sans. | |
| 404 | + | |
| 395 | 405 | ### Les plans disparaissent |
| 396 | 406 | |
| 397 | 407 | En v1, les plans sont stockés en mémoire et seront perdus lorsque : |
| @@ -392,6 +392,16 @@ Si l'agent ne crée pas de plans pour vos demandes multi-étapes : | |||
| 392 | 392 | ||
| 393 | 3. **Vérifiez que la compétence est chargée** - L'agent devrait automatiquement charger la compétence `plan-manager` lorsqu'il détecte une demande de type plan | 393 | 3. **Vérifiez que la compétence est chargée** - L'agent devrait automatiquement charger la compétence `plan-manager` lorsqu'il détecte une demande de type plan |
| 394 | 394 | ||
| 395 | +### L'agent appelle `plan update` en boucle | ||
| 396 | + | ||
| 397 | +`update` change l'état d'une tâche ; il ne peut pas modifier les tâches d'un plan. Un modèle qui envoie un `update` sans `status` valide — souvent avec une liste `tasks`, pour essayer de modifier le plan — reçoit une réponse qui cite son appel, nomme le champ manquant, précise que `tasks` n'est lu que par `create`, et donne un appel valide : | ||
| 398 | + | ||
| 399 | +``` | ||
| 400 | +[error: plan update changes the status of ONE task, and "status" is missing. You sent: {…}. "status" must be one of: open, in_progress, done, failed. "tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again. A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}] | ||
| 401 | +``` | ||
| 402 | + | ||
| 403 | +Si un petit modèle répète quand même l'appel, mettez `planTool: false` pour un agent dont le métier n'a pas besoin de plans (écrire un document, répondre à des questions) : le modèle travaille alors sans. | ||
| 404 | + | ||
| 395 | ### Les plans disparaissent | 405 | ### Les plans disparaissent |
| 396 | 406 | ||
| 397 | En v1, les plans sont stockés en mémoire et seront perdus lorsque : | 407 | En v1, les plans sont stockés en mémoire et seront perdus lorsque : |
added
docs/fr/how-to/save-an-answer.md +59 -0 | new file mode 100644 | ||
| @@ -0,0 +1,59 @@ | ||
| 1 | +# Comment sauvegarder une réponse dans un fichier | |
| 2 | + | |
| 3 | +Ce guide montre comment garder une réponse de l'agent — un mini-tutoriel, une explication, un extrait de code — dans un fichier Markdown, sans que le modèle l'écrive une seconde fois. Il suppose que vous utilisez mm dans le terminal ou en mode fenêtre (`-tui`). | |
| 4 | + | |
| 5 | +Il y a deux façons de faire. `/save` est une commande que vous tapez : rien n'est demandé au modèle. `save_answer` est un outil que le modèle appelle quand vous le demandez avec vos propres mots. | |
| 6 | + | |
| 7 | +## Sauvegarder avec la commande | |
| 8 | + | |
| 9 | +1. Posez votre question et attendez la réponse. | |
| 10 | + | |
| 11 | +2. Tapez : | |
| 12 | + | |
| 13 | + ``` | |
| 14 | + /save premier-script-nushell.md | |
| 15 | + ``` | |
| 16 | + | |
| 17 | + mm écrit la dernière réponse telle quelle et affiche : | |
| 18 | + | |
| 19 | + ``` | |
| 20 | + 💾 saved the last answer to premier-script-nushell.md (2418 bytes) | |
| 21 | + ``` | |
| 22 | + | |
| 23 | +3. Pour sauvegarder une réponse plus ancienne, ajoutez son rang depuis la fin : `/save app-rust.md 2` sauvegarde l'avant-dernière réponse. | |
| 24 | + | |
| 25 | +## Sauvegarder en le demandant à l'agent | |
| 26 | + | |
| 27 | +1. Activez l'outil dans le fichier d'agent : | |
| 28 | + | |
| 29 | + ```yaml | |
| 30 | + saveAnswerTool: true | |
| 31 | + ``` | |
| 32 | + | |
| 33 | +2. Demandez-le avec vos propres mots, après la réponse ou dans la même demande : | |
| 34 | + | |
| 35 | + ``` | |
| 36 | + sauvegarde-le dans premier-script-nushell.md | |
| 37 | + ``` | |
| 38 | + | |
| 39 | + ``` | |
| 40 | + apprends-moi le pattern matching en Kotlin et sauvegarde-le dans kotlin-match.md | |
| 41 | + ``` | |
| 42 | + | |
| 43 | + Le modèle n'envoie que le nom du fichier ; mm écrit le texte que le modèle a déjà donné. Le terminal affiche : | |
| 44 | + | |
| 45 | + ``` | |
| 46 | + 💾 save_answer: premier-script-nushell.md (the previous answer) | |
| 47 | + ``` | |
| 48 | + | |
| 49 | +## Variantes | |
| 50 | + | |
| 51 | +- **Le fichier existe déjà.** Ni `/save` ni `save_answer` ne l'écrasent : ils le disent et n'écrivent rien. Donnez un autre nom, ou supprimez d'abord le fichier. | |
| 52 | +- **Un répertoire qui n'existe pas encore.** `/save notes/rust/app.md` crée `notes/rust/`. | |
| 53 | +- **Vous voulez confirmer chaque sauvegarde.** Avec `yolo: false`, `save_answer` demande avant d'écrire, comme `write_file`. `/save` ne demande jamais : c'est vous qui l'avez tapé. | |
| 54 | +- **Dans un éditeur (ACP).** `/save` n'y est pas proposé ; `save_answer` fonctionne, et écrit dans le répertoire de la session. | |
| 55 | + | |
| 56 | +## Voir aussi | |
| 57 | + | |
| 58 | +- La commande : [commandes slash](../reference/slash-commands.md) | |
| 59 | +- L'outil : [`save_answer`](../reference/tools.md#save_answer) et [`saveAnswerTool`](../reference/configuration.md) | |
| new file mode 100644 | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | +# Comment sauvegarder une réponse dans un fichier | ||
| 2 | + | ||
| 3 | +Ce guide montre comment garder une réponse de l'agent — un mini-tutoriel, une explication, un extrait de code — dans un fichier Markdown, sans que le modèle l'écrive une seconde fois. Il suppose que vous utilisez mm dans le terminal ou en mode fenêtre (`-tui`). | ||
| 4 | + | ||
| 5 | +Il y a deux façons de faire. `/save` est une commande que vous tapez : rien n'est demandé au modèle. `save_answer` est un outil que le modèle appelle quand vous le demandez avec vos propres mots. | ||
| 6 | + | ||
| 7 | +## Sauvegarder avec la commande | ||
| 8 | + | ||
| 9 | +1. Posez votre question et attendez la réponse. | ||
| 10 | + | ||
| 11 | +2. Tapez : | ||
| 12 | + | ||
| 13 | + ``` | ||
| 14 | + /save premier-script-nushell.md | ||
| 15 | + ``` | ||
| 16 | + | ||
| 17 | + mm écrit la dernière réponse telle quelle et affiche : | ||
| 18 | + | ||
| 19 | + ``` | ||
| 20 | + 💾 saved the last answer to premier-script-nushell.md (2418 bytes) | ||
| 21 | + ``` | ||
| 22 | + | ||
| 23 | +3. Pour sauvegarder une réponse plus ancienne, ajoutez son rang depuis la fin : `/save app-rust.md 2` sauvegarde l'avant-dernière réponse. | ||
| 24 | + | ||
| 25 | +## Sauvegarder en le demandant à l'agent | ||
| 26 | + | ||
| 27 | +1. Activez l'outil dans le fichier d'agent : | ||
| 28 | + | ||
| 29 | + ```yaml | ||
| 30 | + saveAnswerTool: true | ||
| 31 | + ``` | ||
| 32 | + | ||
| 33 | +2. Demandez-le avec vos propres mots, après la réponse ou dans la même demande : | ||
| 34 | + | ||
| 35 | + ``` | ||
| 36 | + sauvegarde-le dans premier-script-nushell.md | ||
| 37 | + ``` | ||
| 38 | + | ||
| 39 | + ``` | ||
| 40 | + apprends-moi le pattern matching en Kotlin et sauvegarde-le dans kotlin-match.md | ||
| 41 | + ``` | ||
| 42 | + | ||
| 43 | + Le modèle n'envoie que le nom du fichier ; mm écrit le texte que le modèle a déjà donné. Le terminal affiche : | ||
| 44 | + | ||
| 45 | + ``` | ||
| 46 | + 💾 save_answer: premier-script-nushell.md (the previous answer) | ||
| 47 | + ``` | ||
| 48 | + | ||
| 49 | +## Variantes | ||
| 50 | + | ||
| 51 | +- **Le fichier existe déjà.** Ni `/save` ni `save_answer` ne l'écrasent : ils le disent et n'écrivent rien. Donnez un autre nom, ou supprimez d'abord le fichier. | ||
| 52 | +- **Un répertoire qui n'existe pas encore.** `/save notes/rust/app.md` crée `notes/rust/`. | ||
| 53 | +- **Vous voulez confirmer chaque sauvegarde.** Avec `yolo: false`, `save_answer` demande avant d'écrire, comme `write_file`. `/save` ne demande jamais : c'est vous qui l'avez tapé. | ||
| 54 | +- **Dans un éditeur (ACP).** `/save` n'y est pas proposé ; `save_answer` fonctionne, et écrit dans le répertoire de la session. | ||
| 55 | + | ||
| 56 | +## Voir aussi | ||
| 57 | + | ||
| 58 | +- La commande : [commandes slash](../reference/slash-commands.md) | ||
| 59 | +- L'outil : [`save_answer`](../reference/tools.md#save_answer) et [`saveAnswerTool`](../reference/configuration.md) | ||
modified
docs/fr/reference/configuration.md +2 -1 | @@ -24,6 +24,7 @@ | ||
| 24 | 24 | | `editTools` | booléen | `true` | Déclare `read_file`, `write_file`, `edit_file`. `false` ne laisse que `bash` et `read_skill`. | |
| 25 | 25 | | `fetchContentTool` | booléen | `false` | Déclare `fetch_content`, qui récupère des pages web en Markdown. Désactivé par défaut : c'est le seul outil intégré qui accède au réseau. | |
| 26 | 26 | | `askUserTool` | booléen | `true` | Déclare `ask_user`, par lequel le modèle pose des questions à l'utilisateur (choisir une option, en choisir plusieurs, taper un texte, oui ou non) et attend les réponses. `false` le retire : le modèle ne peut pas demander. Jamais déclaré à côté d'un `structuredOutput`. Surchargé par `AGENT_ASK_USER` ; pour un agent interrogé par un autre, aussi par le `mmConfigs.<nom>.askUser` de celui qui l'interroge. Voir [outils](tools.md#ask_user). | |
| 27 | +| `saveAnswerTool` | bool | `false` | Déclare `save_answer`, qui écrit dans un nouveau fichier une réponse que le modèle a déjà donnée : le modèle envoie un nom de fichier, pas le texte une seconde fois. Désactivé par défaut. La commande `/save` existe dans tous les cas. Voir les [outils](tools.md#save_answer). | | |
| 27 | 28 | | `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. Une clé que l'API OpenAI ne déclare pas est envoyée telle quelle, au premier niveau de la requête — par exemple `chat_template_kwargs: {enable_thinking: false}`, qui demande à un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`) de ne pas raisonner ; `reasoning_effort: none` fait de même avec Ollama. `messages`, `tools`, `tool_choice` (et les anciens `functions`, `function_call`) sont ignorées : Genkit les construit à partir de la conversation. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). | |
| 28 | 29 | | `watchdogTimeout` | durée | `20s` | Silence toléré entre deux tokens diffusés avant de déclarer la connexion bloquée. Doublé pour les requêtes de résumé. | |
| 29 | 30 | | `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama n'envoie pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). | |
| @@ -126,7 +127,7 @@ Le fichier livré `agent.lsp.yaml` est `agent.dmr.yaml` avec ce bloc actif (gopl | ||
| 126 | 127 | | Clé | Type | Défaut | Description | |
| 127 | 128 | |-----|------|--------|-------------| |
| 128 | 129 | | `tools` | liste | `[]` | Une entrée par outil déclaré. | |
| 129 | -| `tools[].name` | chaîne | — | Le nom que le modèle appelle. Obligatoire. Commence par une lettre ; lettres, chiffres, `_`, `-` ; 64 caractères au plus. Unique. Pas le nom d'un outil intégré (`ask_agent`, `ask_user`, `bash`, `edit_file`, `fetch_content`, `lsp`, `plan`, `read_file`, `read_skill`, `write_file`), même désactivé dans cet agent. | | |
| 130 | +| `tools[].name` | chaîne | — | Le nom que le modèle appelle. Obligatoire. Commence par une lettre ; lettres, chiffres, `_`, `-` ; 64 caractères au plus. Unique. Pas le nom d'un outil intégré (`ask_agent`, `ask_user`, `bash`, `edit_file`, `fetch_content`, `lsp`, `plan`, `read_file`, `read_skill`, `save_answer`, `write_file`), même désactivé dans cet agent. | | |
| 130 | 131 | | `tools[].description` | chaîne | — | Ce que fait l'outil et quand s'en servir, montré au modèle. Obligatoire. Une ligne `Required parameters: …` y est ajoutée. | |
| 131 | 132 | | `tools[].parameters` | map nom → paramètre | `{}` | Les arguments, dans l'ordre écrit. Les noms suivent les règles de `name`. | |
| 132 | 133 | | `tools[].parameters.<p>.type` | chaîne | `string` | `string`, `number`, `integer` ou `boolean`. | |
| @@ -24,6 +24,7 @@ | |||
| 24 | | `editTools` | booléen | `true` | Déclare `read_file`, `write_file`, `edit_file`. `false` ne laisse que `bash` et `read_skill`. | | 24 | | `editTools` | booléen | `true` | Déclare `read_file`, `write_file`, `edit_file`. `false` ne laisse que `bash` et `read_skill`. | |
| 25 | | `fetchContentTool` | booléen | `false` | Déclare `fetch_content`, qui récupère des pages web en Markdown. Désactivé par défaut : c'est le seul outil intégré qui accède au réseau. | | 25 | | `fetchContentTool` | booléen | `false` | Déclare `fetch_content`, qui récupère des pages web en Markdown. Désactivé par défaut : c'est le seul outil intégré qui accède au réseau. | |
| 26 | | `askUserTool` | booléen | `true` | Déclare `ask_user`, par lequel le modèle pose des questions à l'utilisateur (choisir une option, en choisir plusieurs, taper un texte, oui ou non) et attend les réponses. `false` le retire : le modèle ne peut pas demander. Jamais déclaré à côté d'un `structuredOutput`. Surchargé par `AGENT_ASK_USER` ; pour un agent interrogé par un autre, aussi par le `mmConfigs.<nom>.askUser` de celui qui l'interroge. Voir [outils](tools.md#ask_user). | | 26 | | `askUserTool` | booléen | `true` | Déclare `ask_user`, par lequel le modèle pose des questions à l'utilisateur (choisir une option, en choisir plusieurs, taper un texte, oui ou non) et attend les réponses. `false` le retire : le modèle ne peut pas demander. Jamais déclaré à côté d'un `structuredOutput`. Surchargé par `AGENT_ASK_USER` ; pour un agent interrogé par un autre, aussi par le `mmConfigs.<nom>.askUser` de celui qui l'interroge. Voir [outils](tools.md#ask_user). | |
| 27 | +| `saveAnswerTool` | bool | `false` | Déclare `save_answer`, qui écrit dans un nouveau fichier une réponse que le modèle a déjà donnée : le modèle envoie un nom de fichier, pas le texte une seconde fois. Désactivé par défaut. La commande `/save` existe dans tous les cas. Voir les [outils](tools.md#save_answer). | | ||
| 27 | | `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. Une clé que l'API OpenAI ne déclare pas est envoyée telle quelle, au premier niveau de la requête — par exemple `chat_template_kwargs: {enable_thinking: false}`, qui demande à un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`) de ne pas raisonner ; `reasoning_effort: none` fait de même avec Ollama. `messages`, `tools`, `tool_choice` (et les anciens `functions`, `function_call`) sont ignorées : Genkit les construit à partir de la conversation. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). | | 28 | | `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. Une clé que l'API OpenAI ne déclare pas est envoyée telle quelle, au premier niveau de la requête — par exemple `chat_template_kwargs: {enable_thinking: false}`, qui demande à un serveur llama.cpp (`dmr`, `llamacpp`, `llmman`) de ne pas raisonner ; `reasoning_effort: none` fait de même avec Ollama. `messages`, `tools`, `tool_choice` (et les anciens `functions`, `function_call`) sont ignorées : Genkit les construit à partir de la conversation. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). | |
| 28 | | `watchdogTimeout` | durée | `20s` | Silence toléré entre deux tokens diffusés avant de déclarer la connexion bloquée. Doublé pour les requêtes de résumé. | | 29 | | `watchdogTimeout` | durée | `20s` | Silence toléré entre deux tokens diffusés avant de déclarer la connexion bloquée. Doublé pour les requêtes de résumé. | |
| 29 | | `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama n'envoie pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). | | 30 | | `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama n'envoie pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). | |
| @@ -126,7 +127,7 @@ Le fichier livré `agent.lsp.yaml` est `agent.dmr.yaml` avec ce bloc actif (gopl | |||
| 126 | | Clé | Type | Défaut | Description | | 127 | | Clé | Type | Défaut | Description | |
| 127 | |-----|------|--------|-------------| | 128 | |-----|------|--------|-------------| |
| 128 | | `tools` | liste | `[]` | Une entrée par outil déclaré. | | 129 | | `tools` | liste | `[]` | Une entrée par outil déclaré. | |
| 129 | -| `tools[].name` | chaîne | — | Le nom que le modèle appelle. Obligatoire. Commence par une lettre ; lettres, chiffres, `_`, `-` ; 64 caractères au plus. Unique. Pas le nom d'un outil intégré (`ask_agent`, `ask_user`, `bash`, `edit_file`, `fetch_content`, `lsp`, `plan`, `read_file`, `read_skill`, `write_file`), même désactivé dans cet agent. | | 130 | +| `tools[].name` | chaîne | — | Le nom que le modèle appelle. Obligatoire. Commence par une lettre ; lettres, chiffres, `_`, `-` ; 64 caractères au plus. Unique. Pas le nom d'un outil intégré (`ask_agent`, `ask_user`, `bash`, `edit_file`, `fetch_content`, `lsp`, `plan`, `read_file`, `read_skill`, `save_answer`, `write_file`), même désactivé dans cet agent. | |
| 130 | | `tools[].description` | chaîne | — | Ce que fait l'outil et quand s'en servir, montré au modèle. Obligatoire. Une ligne `Required parameters: …` y est ajoutée. | | 131 | | `tools[].description` | chaîne | — | Ce que fait l'outil et quand s'en servir, montré au modèle. Obligatoire. Une ligne `Required parameters: …` y est ajoutée. | |
| 131 | | `tools[].parameters` | map nom → paramètre | `{}` | Les arguments, dans l'ordre écrit. Les noms suivent les règles de `name`. | | 132 | | `tools[].parameters` | map nom → paramètre | `{}` | Les arguments, dans l'ordre écrit. Les noms suivent les règles de `name`. | |
| 132 | | `tools[].parameters.<p>.type` | chaîne | `string` | `string`, `number`, `integer` ou `boolean`. | | 133 | | `tools[].parameters.<p>.type` | chaîne | `string` | `string`, `number`, `integer` ou `boolean`. | |
modified
docs/fr/reference/slash-commands.md +1 -0 | @@ -13,6 +13,7 @@ | ||
| 13 | 13 | | `/sessions` | Liste les sessions enregistrées du projet, la plus récente d'abord : identifiant, dernière mise à jour, nombre de questions, première question, puis ` · ⚠️ N error(s)` quand la session a enregistré des erreurs ; `*` marque la session courante. Avec `sessions.enabled: false` : `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Envoyée au modèle comme nouveau message. | |
| 14 | 14 | | `/resume [id]` | Reprend une session enregistrée : la plus récemment mise à jour autre que la courante, en ignorant les vides, ou l'identifiant donné. Affiche `↩️ resumed <id> — N message(s), started with "<première question>"` (suivi de ` · ⚠️ N error(s)` quand la session a enregistré des erreurs) ; réinitialise le détecteur de boucles et le compte de tokens du serveur ; les tours suivants sont enregistrés dans cette session. Refusée avec une raison `⚠️` quand les sessions sont désactivées, qu'il n'y a pas de session antérieure, que l'identifiant est inconnu ou désigne la session courante. | Envoyée au modèle comme nouveau message. | |
| 15 | 15 | | `/help` | Affiche les commandes de cette façade, une par ligne avec leur description, alignées. | Envoyée au modèle comme nouveau message. | |
| 16 | +| `/save <fichier> [n]` | Écrit la dernière réponse (`n` : la n-ième depuis la fin) telle quelle dans un nouveau fichier, en créant les répertoires manquants, et affiche `💾 saved the last answer to <fichier> (N bytes)`. N'écrase jamais : un fichier existant donne `⚠️ <fichier> already exists — nothing was written; choose another name`. Un nom de fichier avec des espaces n'est pas pris en charge. Le modèle n'est pas appelé. | Envoyée au modèle comme nouveau message. | | |
| 16 | 17 | | `/copy [n\|all]` | Copie la dernière réponse (`n` : la n-ième depuis la fin) dans le presse-papiers via `pbcopy`/`wl-copy`/`xclip`/`xsel` ou, à défaut, OSC 52 quand stdout est un terminal, et affiche `📋 copied N chars (<chemin>)` ou `⚠️ …`. `/copy all` est réservé au mode fenêtre : ici la commande le dit. | Envoyée au modèle comme nouveau message. | |
| 17 | 18 | |
| 18 | 19 | Avec `sessions.enabled`, `/new` laisse le fichier de la session courante tel quel ; la question suivante ouvre un nouveau fichier. Une ligne vide à l'invite est ignorée. |
| @@ -13,6 +13,7 @@ | |||
| 13 | | `/sessions` | Liste les sessions enregistrées du projet, la plus récente d'abord : identifiant, dernière mise à jour, nombre de questions, première question, puis ` · ⚠️ N error(s)` quand la session a enregistré des erreurs ; `*` marque la session courante. Avec `sessions.enabled: false` : `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Envoyée au modèle comme nouveau message. | | 13 | | `/sessions` | Liste les sessions enregistrées du projet, la plus récente d'abord : identifiant, dernière mise à jour, nombre de questions, première question, puis ` · ⚠️ N error(s)` quand la session a enregistré des erreurs ; `*` marque la session courante. Avec `sessions.enabled: false` : `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Envoyée au modèle comme nouveau message. | |
| 14 | | `/resume [id]` | Reprend une session enregistrée : la plus récemment mise à jour autre que la courante, en ignorant les vides, ou l'identifiant donné. Affiche `↩️ resumed <id> — N message(s), started with "<première question>"` (suivi de ` · ⚠️ N error(s)` quand la session a enregistré des erreurs) ; réinitialise le détecteur de boucles et le compte de tokens du serveur ; les tours suivants sont enregistrés dans cette session. Refusée avec une raison `⚠️` quand les sessions sont désactivées, qu'il n'y a pas de session antérieure, que l'identifiant est inconnu ou désigne la session courante. | Envoyée au modèle comme nouveau message. | | 14 | | `/resume [id]` | Reprend une session enregistrée : la plus récemment mise à jour autre que la courante, en ignorant les vides, ou l'identifiant donné. Affiche `↩️ resumed <id> — N message(s), started with "<première question>"` (suivi de ` · ⚠️ N error(s)` quand la session a enregistré des erreurs) ; réinitialise le détecteur de boucles et le compte de tokens du serveur ; les tours suivants sont enregistrés dans cette session. Refusée avec une raison `⚠️` quand les sessions sont désactivées, qu'il n'y a pas de session antérieure, que l'identifiant est inconnu ou désigne la session courante. | Envoyée au modèle comme nouveau message. | |
| 15 | | `/help` | Affiche les commandes de cette façade, une par ligne avec leur description, alignées. | Envoyée au modèle comme nouveau message. | | 15 | | `/help` | Affiche les commandes de cette façade, une par ligne avec leur description, alignées. | Envoyée au modèle comme nouveau message. | |
| 16 | +| `/save <fichier> [n]` | Écrit la dernière réponse (`n` : la n-ième depuis la fin) telle quelle dans un nouveau fichier, en créant les répertoires manquants, et affiche `💾 saved the last answer to <fichier> (N bytes)`. N'écrase jamais : un fichier existant donne `⚠️ <fichier> already exists — nothing was written; choose another name`. Un nom de fichier avec des espaces n'est pas pris en charge. Le modèle n'est pas appelé. | Envoyée au modèle comme nouveau message. | | ||
| 16 | | `/copy [n\|all]` | Copie la dernière réponse (`n` : la n-ième depuis la fin) dans le presse-papiers via `pbcopy`/`wl-copy`/`xclip`/`xsel` ou, à défaut, OSC 52 quand stdout est un terminal, et affiche `📋 copied N chars (<chemin>)` ou `⚠️ …`. `/copy all` est réservé au mode fenêtre : ici la commande le dit. | Envoyée au modèle comme nouveau message. | | 17 | | `/copy [n\|all]` | Copie la dernière réponse (`n` : la n-ième depuis la fin) dans le presse-papiers via `pbcopy`/`wl-copy`/`xclip`/`xsel` ou, à défaut, OSC 52 quand stdout est un terminal, et affiche `📋 copied N chars (<chemin>)` ou `⚠️ …`. `/copy all` est réservé au mode fenêtre : ici la commande le dit. | Envoyée au modèle comme nouveau message. | |
| 17 | 18 | ||
| 18 | Avec `sessions.enabled`, `/new` laisse le fichier de la session courante tel quel ; la question suivante ouvre un nouveau fichier. Une ligne vide à l'invite est ignorée. | 19 | Avec `sessions.enabled`, `/new` laisse le fichier de la session courante tel quel ; la question suivante ouvre un nouveau fichier. Une ligne vide à l'invite est ignorée. |
modified
docs/fr/reference/tools.md +20 -0 | @@ -248,6 +248,26 @@ Le schéma déclaré décrit les champs d'une question en mots, sans `properties | ||
| 248 | 248 | | Résultat | Répondu : `The user answered:` puis une ligne `- <question> → <réponse>` par question (plusieurs options jointes par `, `). Refusé : `[the user declined to answer: do not ask the same thing again; go on without it or stop]`. Annulé : `[the question was cancelled before the user answered]`. Personne à interroger : `[the user cannot be asked in this mode: make a reasonable assumption, say which, and go on]`. Tout ce qui n'est pas une réponse termine l'appel en échec dans la fenêtre et l'éditeur. | |
| 249 | 249 | | Erreurs | `[ask_user: no question — send questions: [{question, type, options}]]` ; `[ask_user: N questions — ask at most 4 at a time]` ; `[ask_user: question N has no text]` ; `[ask_user: question N is a choice question — give it at least two options]` (ou `multi`) ; `[ask_user: question N has type "x" — use choice, multi, text or confirm]`. | |
| 250 | 250 | |
| 251 | +## `save_answer` | |
| 252 | + | |
| 253 | +Déclaré quand `saveAnswerTool: true` (défaut `false`). | |
| 254 | + | |
| 255 | +| Champ | Type | Description | | |
| 256 | +|-------|------|-------------| | |
| 257 | +| `path` | chaîne | Le fichier à créer, relatif au répertoire de travail. Il ne doit pas encore exister. | | |
| 258 | + | |
| 259 | +| Aspect | Comportement | | |
| 260 | +|--------|--------------| | |
| 261 | +| Texte écrit | Le texte du message du modèle qui a fait l'appel, s'il en a un ; sinon la dernière réponse de la conversation telle qu'elle était au début du tour. Écrit tel quel, avec un saut de ligne final ; le modèle ne le génère pas une seconde fois. | | |
| 262 | +| Fichier existant | Jamais écrasé : `[save_answer: <path> already exists — nothing was written. Pick another name, or ask the user]`. | | |
| 263 | +| Pas encore de réponse | `[save_answer: there is no answer to save yet — write the answer in your reply first, then call save_answer with the file name]` ; rien n'est demandé. | | |
| 264 | +| Répertoires | Les répertoires parents manquants sont créés. | | |
| 265 | +| Résultat | `saved <this answer\|the previous answer> to <path> (N bytes), starting with: <première ligne>`. | | |
| 266 | +| Répertoire de travail | Le répertoire du processus en mode terminal ; le `cwd` de la session en mode ACP et dans la fenêtre. | | |
| 267 | +| Permission | Demandée quand `yolo: false`, comme pour `write_file` ; un refus renvoie `[save_answer rejected by the user]`. | | |
| 268 | +| Affichage terminal | `💾 save_answer: <path> (this answer\|the previous answer)`, puis le résultat. | | |
| 269 | +| ACP et fenêtre | Appel d'outil de type `edit`, avec le chemin absolu du fichier. | | |
| 270 | + | |
| 251 | 271 | ## Outils déclarés |
| 252 | 272 | |
| 253 | 273 | Déclarés à partir du bloc `tools:` du fichier d'agent, un outil par entrée (voir la [configuration](configuration.md#clés-de-tools)). Chacun porte le `name` de l'entrée ; ses champs sont les `parameters` de l'entrée. |
| @@ -248,6 +248,26 @@ Le schéma déclaré décrit les champs d'une question en mots, sans `properties | |||
| 248 | | Résultat | Répondu : `The user answered:` puis une ligne `- <question> → <réponse>` par question (plusieurs options jointes par `, `). Refusé : `[the user declined to answer: do not ask the same thing again; go on without it or stop]`. Annulé : `[the question was cancelled before the user answered]`. Personne à interroger : `[the user cannot be asked in this mode: make a reasonable assumption, say which, and go on]`. Tout ce qui n'est pas une réponse termine l'appel en échec dans la fenêtre et l'éditeur. | | 248 | | Résultat | Répondu : `The user answered:` puis une ligne `- <question> → <réponse>` par question (plusieurs options jointes par `, `). Refusé : `[the user declined to answer: do not ask the same thing again; go on without it or stop]`. Annulé : `[the question was cancelled before the user answered]`. Personne à interroger : `[the user cannot be asked in this mode: make a reasonable assumption, say which, and go on]`. Tout ce qui n'est pas une réponse termine l'appel en échec dans la fenêtre et l'éditeur. | |
| 249 | | Erreurs | `[ask_user: no question — send questions: [{question, type, options}]]` ; `[ask_user: N questions — ask at most 4 at a time]` ; `[ask_user: question N has no text]` ; `[ask_user: question N is a choice question — give it at least two options]` (ou `multi`) ; `[ask_user: question N has type "x" — use choice, multi, text or confirm]`. | | 249 | | Erreurs | `[ask_user: no question — send questions: [{question, type, options}]]` ; `[ask_user: N questions — ask at most 4 at a time]` ; `[ask_user: question N has no text]` ; `[ask_user: question N is a choice question — give it at least two options]` (ou `multi`) ; `[ask_user: question N has type "x" — use choice, multi, text or confirm]`. | |
| 250 | 250 | ||
| 251 | +## `save_answer` | ||
| 252 | + | ||
| 253 | +Déclaré quand `saveAnswerTool: true` (défaut `false`). | ||
| 254 | + | ||
| 255 | +| Champ | Type | Description | | ||
| 256 | +|-------|------|-------------| | ||
| 257 | +| `path` | chaîne | Le fichier à créer, relatif au répertoire de travail. Il ne doit pas encore exister. | | ||
| 258 | + | ||
| 259 | +| Aspect | Comportement | | ||
| 260 | +|--------|--------------| | ||
| 261 | +| Texte écrit | Le texte du message du modèle qui a fait l'appel, s'il en a un ; sinon la dernière réponse de la conversation telle qu'elle était au début du tour. Écrit tel quel, avec un saut de ligne final ; le modèle ne le génère pas une seconde fois. | | ||
| 262 | +| Fichier existant | Jamais écrasé : `[save_answer: <path> already exists — nothing was written. Pick another name, or ask the user]`. | | ||
| 263 | +| Pas encore de réponse | `[save_answer: there is no answer to save yet — write the answer in your reply first, then call save_answer with the file name]` ; rien n'est demandé. | | ||
| 264 | +| Répertoires | Les répertoires parents manquants sont créés. | | ||
| 265 | +| Résultat | `saved <this answer\|the previous answer> to <path> (N bytes), starting with: <première ligne>`. | | ||
| 266 | +| Répertoire de travail | Le répertoire du processus en mode terminal ; le `cwd` de la session en mode ACP et dans la fenêtre. | | ||
| 267 | +| Permission | Demandée quand `yolo: false`, comme pour `write_file` ; un refus renvoie `[save_answer rejected by the user]`. | | ||
| 268 | +| Affichage terminal | `💾 save_answer: <path> (this answer\|the previous answer)`, puis le résultat. | | ||
| 269 | +| ACP et fenêtre | Appel d'outil de type `edit`, avec le chemin absolu du fichier. | | ||
| 270 | + | ||
| 251 | ## Outils déclarés | 271 | ## Outils déclarés |
| 252 | 272 | ||
| 253 | Déclarés à partir du bloc `tools:` du fichier d'agent, un outil par entrée (voir la [configuration](configuration.md#clés-de-tools)). Chacun porte le `name` de l'entrée ; ses champs sont les `parameters` de l'entrée. | 273 | Déclarés à partir du bloc `tools:` du fichier d'agent, un outil par entrée (voir la [configuration](configuration.md#clés-de-tools)). Chacun porte le `name` de l'entrée ; ses champs sont les `parameters` de l'entrée. |
modified
install.sh +2 -0 | @@ -4,6 +4,8 @@ set -euo pipefail | ||
| 4 | 4 | BINARY_NAME="mm" |
| 5 | 5 | INSTALL_DIR="/usr/local/bin" |
| 6 | 6 | |
| 7 | +rm "${INSTALL_DIR}/mm" | |
| 8 | + | |
| 7 | 9 | echo "Building ${BINARY_NAME}..." |
| 8 | 10 | go build -o "${BINARY_NAME}" . |
| 9 | 11 | |
| @@ -4,6 +4,8 @@ set -euo pipefail | |||
| 4 | BINARY_NAME="mm" | 4 | BINARY_NAME="mm" |
| 5 | INSTALL_DIR="/usr/local/bin" | 5 | INSTALL_DIR="/usr/local/bin" |
| 6 | 6 | ||
| 7 | +rm "${INSTALL_DIR}/mm" | ||
| 8 | + | ||
| 7 | echo "Building ${BINARY_NAME}..." | 9 | echo "Building ${BINARY_NAME}..." |
| 8 | go build -o "${BINARY_NAME}" . | 10 | go build -o "${BINARY_NAME}" . |
| 9 | 11 | ||
modified
internal/agent/agent.go +3 -0 | @@ -21,6 +21,7 @@ import ( | ||
| 21 | 21 | "mm/internal/elicit" |
| 22 | 22 | "mm/internal/engine" |
| 23 | 23 | "mm/internal/mention" |
| 24 | + "mm/internal/saved" | |
| 24 | 25 | "mm/internal/session" |
| 25 | 26 | "mm/internal/skills" |
| 26 | 27 | "mm/internal/spinner" |
| @@ -536,6 +537,8 @@ func (l *loop) historyCommand(input string, messages []*ai.Message) ([]*ai.Messa | ||
| 536 | 537 | fmt.Println(command.Usage(command.Terminal)) |
| 537 | 538 | case command.Copy: |
| 538 | 539 | fmt.Println(copyAnswer(messages, arg, clipboard.New(terminalOSC52()))) |
| 540 | + case command.Save: | |
| 541 | + fmt.Println(saved.Command(modelAnswers(messages), arg)) | |
| 539 | 542 | case command.Resume: |
| 540 | 543 | resumed, line, err := l.keep.Resume(arg, agentsmd.System(l.system, "")) |
| 541 | 544 | if err != nil { |
| @@ -21,6 +21,7 @@ import ( | |||
| 21 | "mm/internal/elicit" | 21 | "mm/internal/elicit" |
| 22 | "mm/internal/engine" | 22 | "mm/internal/engine" |
| 23 | "mm/internal/mention" | 23 | "mm/internal/mention" |
| 24 | + "mm/internal/saved" | ||
| 24 | "mm/internal/session" | 25 | "mm/internal/session" |
| 25 | "mm/internal/skills" | 26 | "mm/internal/skills" |
| 26 | "mm/internal/spinner" | 27 | "mm/internal/spinner" |
| @@ -536,6 +537,8 @@ func (l *loop) historyCommand(input string, messages []*ai.Message) ([]*ai.Messa | |||
| 536 | fmt.Println(command.Usage(command.Terminal)) | 537 | fmt.Println(command.Usage(command.Terminal)) |
| 537 | case command.Copy: | 538 | case command.Copy: |
| 538 | fmt.Println(copyAnswer(messages, arg, clipboard.New(terminalOSC52()))) | 539 | fmt.Println(copyAnswer(messages, arg, clipboard.New(terminalOSC52()))) |
| 540 | + case command.Save: | ||
| 541 | + fmt.Println(saved.Command(modelAnswers(messages), arg)) | ||
| 539 | case command.Resume: | 542 | case command.Resume: |
| 540 | resumed, line, err := l.keep.Resume(arg, agentsmd.System(l.system, "")) | 543 | resumed, line, err := l.keep.Resume(arg, agentsmd.System(l.system, "")) |
| 541 | if err != nil { | 544 | if err != nil { |
modified
internal/agent/copy.go +4 -13 | @@ -4,9 +4,9 @@ import ( | ||
| 4 | 4 | "fmt" |
| 5 | 5 | "os" |
| 6 | 6 | "strconv" |
| 7 | - "strings" | |
| 8 | 7 | |
| 9 | 8 | "mm/internal/clipboard" |
| 9 | + "mm/internal/saved" | |
| 10 | 10 | |
| 11 | 11 | "github.com/firebase/genkit/go/ai" |
| 12 | 12 | ) |
| @@ -44,19 +44,10 @@ type copier interface { | ||
| 44 | 44 | Copy(text string) (how string, err error) |
| 45 | 45 | } |
| 46 | 46 | |
| 47 | -// modelAnswers is the text of every model message that spoke to the user — | |
| 48 | -// tool-call-only messages have none — oldest first. | |
| 47 | +// modelAnswers is the text of every model message that spoke to the user, | |
| 48 | +// oldest first (saved.Answers: /copy and /save count answers alike). | |
| 49 | 49 | func modelAnswers(messages []*ai.Message) []string { |
| 50 | - var out []string | |
| 51 | - for _, m := range messages { | |
| 52 | - if m.Role != ai.RoleModel { | |
| 53 | - continue | |
| 54 | - } | |
| 55 | - if text := strings.TrimSpace(m.Text()); text != "" { | |
| 56 | - out = append(out, text) | |
| 57 | - } | |
| 58 | - } | |
| 59 | - return out | |
| 50 | + return saved.Answers(messages) | |
| 60 | 51 | } |
| 61 | 52 | |
| 62 | 53 | // terminalOSC52 is the REPL's OSC 52 sink: the sequence goes to stdout when |
| @@ -4,9 +4,9 @@ import ( | |||
| 4 | "fmt" | 4 | "fmt" |
| 5 | "os" | 5 | "os" |
| 6 | "strconv" | 6 | "strconv" |
| 7 | - "strings" | ||
| 8 | 7 | ||
| 9 | "mm/internal/clipboard" | 8 | "mm/internal/clipboard" |
| 9 | + "mm/internal/saved" | ||
| 10 | 10 | ||
| 11 | "github.com/firebase/genkit/go/ai" | 11 | "github.com/firebase/genkit/go/ai" |
| 12 | ) | 12 | ) |
| @@ -44,19 +44,10 @@ type copier interface { | |||
| 44 | Copy(text string) (how string, err error) | 44 | Copy(text string) (how string, err error) |
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | -// modelAnswers is the text of every model message that spoke to the user — | 47 | +// modelAnswers is the text of every model message that spoke to the user, |
| 48 | -// tool-call-only messages have none — oldest first. | 48 | +// oldest first (saved.Answers: /copy and /save count answers alike). |
| 49 | func modelAnswers(messages []*ai.Message) []string { | 49 | func modelAnswers(messages []*ai.Message) []string { |
| 50 | - var out []string | 50 | + return saved.Answers(messages) |
| 51 | - for _, m := range messages { | ||
| 52 | - if m.Role != ai.RoleModel { | ||
| 53 | - continue | ||
| 54 | - } | ||
| 55 | - if text := strings.TrimSpace(m.Text()); text != "" { | ||
| 56 | - out = append(out, text) | ||
| 57 | - } | ||
| 58 | - } | ||
| 59 | - return out | ||
| 60 | } | 51 | } |
| 61 | 52 | ||
| 62 | // terminalOSC52 is the REPL's OSC 52 sink: the sequence goes to stdout when | 53 | // terminalOSC52 is the REPL's OSC 52 sink: the sequence goes to stdout when |
modified
internal/command/command.go +2 -0 | @@ -40,6 +40,7 @@ const ( | ||
| 40 | 40 | Resume = "resume" |
| 41 | 41 | Help = "help" |
| 42 | 42 | Copy = "copy" |
| 43 | + Save = "save" | |
| 43 | 44 | ) |
| 44 | 45 | |
| 45 | 46 | // Command is one slash command: what it is called, what it does, who offers |
| @@ -72,6 +73,7 @@ var table = []Command{ | ||
| 72 | 73 | {Name: Resume, Description: "Continue a stored session: the latest, or the given id", Fronts: Terminal | Window, TakesArg: true}, |
| 73 | 74 | {Name: Help, Description: "List the commands", Fronts: Terminal | Window}, |
| 74 | 75 | {Name: Copy, Description: "Copy the last answer to the clipboard (/copy 2: the one before; /copy all: the whole conversation)", Fronts: Terminal | Window, TakesArg: true}, |
| 76 | + {Name: Save, Description: "Save the last answer to a file, never overwriting one (/save notes.md 2: the answer before)", Fronts: Terminal | Window, TakesArg: true, Hint: "<file> [n]"}, | |
| 75 | 77 | } |
| 76 | 78 | |
| 77 | 79 | // All returns the whole table, in display order: the fixed commands, then |
| @@ -40,6 +40,7 @@ const ( | |||
| 40 | Resume = "resume" | 40 | Resume = "resume" |
| 41 | Help = "help" | 41 | Help = "help" |
| 42 | Copy = "copy" | 42 | Copy = "copy" |
| 43 | + Save = "save" | ||
| 43 | ) | 44 | ) |
| 44 | 45 | ||
| 45 | // Command is one slash command: what it is called, what it does, who offers | 46 | // Command is one slash command: what it is called, what it does, who offers |
| @@ -72,6 +73,7 @@ var table = []Command{ | |||
| 72 | {Name: Resume, Description: "Continue a stored session: the latest, or the given id", Fronts: Terminal | Window, TakesArg: true}, | 73 | {Name: Resume, Description: "Continue a stored session: the latest, or the given id", Fronts: Terminal | Window, TakesArg: true}, |
| 73 | {Name: Help, Description: "List the commands", Fronts: Terminal | Window}, | 74 | {Name: Help, Description: "List the commands", Fronts: Terminal | Window}, |
| 74 | {Name: Copy, Description: "Copy the last answer to the clipboard (/copy 2: the one before; /copy all: the whole conversation)", Fronts: Terminal | Window, TakesArg: true}, | 75 | {Name: Copy, Description: "Copy the last answer to the clipboard (/copy 2: the one before; /copy all: the whole conversation)", Fronts: Terminal | Window, TakesArg: true}, |
| 76 | + {Name: Save, Description: "Save the last answer to a file, never overwriting one (/save notes.md 2: the answer before)", Fronts: Terminal | Window, TakesArg: true, Hint: "<file> [n]"}, | ||
| 75 | } | 77 | } |
| 76 | 78 | ||
| 77 | // All returns the whole table, in display order: the fixed commands, then | 79 | // All returns the whole table, in display order: the fixed commands, then |
modified
internal/command/command_test.go +3 -3 | @@ -19,15 +19,15 @@ func TestForFiltersByFront(t *testing.T) { | ||
| 19 | 19 | if got := names(For(Editor)); got != "compact new" { |
| 20 | 20 | t.Errorf("Editor commands = %q, want %q", got, "compact new") |
| 21 | 21 | } |
| 22 | - want := "quit abort compact new sessions resume help copy" | |
| 22 | + want := "quit abort compact new sessions resume help copy save" | |
| 23 | 23 | if got := names(For(Terminal)); got != want { |
| 24 | 24 | t.Errorf("Terminal commands = %q, want %q", got, want) |
| 25 | 25 | } |
| 26 | 26 | if got := names(For(Window)); got != want { |
| 27 | 27 | t.Errorf("Window commands = %q, want %q", got, want) |
| 28 | 28 | } |
| 29 | - if len(All()) != 8 { | |
| 30 | - t.Errorf("All() has %d commands, want 8", len(All())) | |
| 29 | + if len(All()) != 9 { | |
| 30 | + t.Errorf("All() has %d commands, want 9", len(All())) | |
| 31 | 31 | } |
| 32 | 32 | } |
| 33 | 33 | |
| @@ -19,15 +19,15 @@ func TestForFiltersByFront(t *testing.T) { | |||
| 19 | if got := names(For(Editor)); got != "compact new" { | 19 | if got := names(For(Editor)); got != "compact new" { |
| 20 | t.Errorf("Editor commands = %q, want %q", got, "compact new") | 20 | t.Errorf("Editor commands = %q, want %q", got, "compact new") |
| 21 | } | 21 | } |
| 22 | - want := "quit abort compact new sessions resume help copy" | 22 | + want := "quit abort compact new sessions resume help copy save" |
| 23 | if got := names(For(Terminal)); got != want { | 23 | if got := names(For(Terminal)); got != want { |
| 24 | t.Errorf("Terminal commands = %q, want %q", got, want) | 24 | t.Errorf("Terminal commands = %q, want %q", got, want) |
| 25 | } | 25 | } |
| 26 | if got := names(For(Window)); got != want { | 26 | if got := names(For(Window)); got != want { |
| 27 | t.Errorf("Window commands = %q, want %q", got, want) | 27 | t.Errorf("Window commands = %q, want %q", got, want) |
| 28 | } | 28 | } |
| 29 | - if len(All()) != 8 { | 29 | + if len(All()) != 9 { |
| 30 | - t.Errorf("All() has %d commands, want 8", len(All())) | 30 | + t.Errorf("All() has %d commands, want 9", len(All())) |
| 31 | } | 31 | } |
| 32 | } | 32 | } |
| 33 | 33 | ||
modified
internal/config/config.go +7 -0 | @@ -126,6 +126,13 @@ type Config struct { | ||
| 126 | 126 | // Override: AGENT_ASK_USER. |
| 127 | 127 | AskUserTool bool `yaml:"askUserTool"` |
| 128 | 128 | |
| 129 | + // SaveAnswerTool: declare the built-in `save_answer` tool, which writes an | |
| 130 | + // answer the model already gave to a new file — the model sends a file | |
| 131 | + // name, not the text again (ticket 93). Off by default, so the tool list | |
| 132 | + // of the existing agents does not change. The /save command exists either | |
| 133 | + // way. | |
| 134 | + SaveAnswerTool bool `yaml:"saveAnswerTool"` | |
| 135 | + | |
| 129 | 136 | // Sampling: generation settings (OpenAI API keys; the plugin converts them.) |
| 130 | 137 | Sampling map[string]any `yaml:"sampling"` |
| 131 | 138 | |
| @@ -126,6 +126,13 @@ type Config struct { | |||
| 126 | // Override: AGENT_ASK_USER. | 126 | // Override: AGENT_ASK_USER. |
| 127 | AskUserTool bool `yaml:"askUserTool"` | 127 | AskUserTool bool `yaml:"askUserTool"` |
| 128 | 128 | ||
| 129 | + // SaveAnswerTool: declare the built-in `save_answer` tool, which writes an | ||
| 130 | + // answer the model already gave to a new file — the model sends a file | ||
| 131 | + // name, not the text again (ticket 93). Off by default, so the tool list | ||
| 132 | + // of the existing agents does not change. The /save command exists either | ||
| 133 | + // way. | ||
| 134 | + SaveAnswerTool bool `yaml:"saveAnswerTool"` | ||
| 135 | + | ||
| 129 | // Sampling: generation settings (OpenAI API keys; the plugin converts them.) | 136 | // Sampling: generation settings (OpenAI API keys; the plugin converts them.) |
| 130 | Sampling map[string]any `yaml:"sampling"` | 137 | Sampling map[string]any `yaml:"sampling"` |
| 131 | 138 | ||
modified
internal/config/tools.go +1 -0 | @@ -43,6 +43,7 @@ var BuiltinToolNames = []string{ | ||
| 43 | 43 | "plan", |
| 44 | 44 | "read_file", |
| 45 | 45 | "read_skill", |
| 46 | + "save_answer", | |
| 46 | 47 | "write_file", |
| 47 | 48 | } |
| 48 | 49 | |
| @@ -43,6 +43,7 @@ var BuiltinToolNames = []string{ | |||
| 43 | "plan", | 43 | "plan", |
| 44 | "read_file", | 44 | "read_file", |
| 45 | "read_skill", | 45 | "read_skill", |
| 46 | + "save_answer", | ||
| 46 | "write_file", | 47 | "write_file", |
| 47 | } | 48 | } |
| 48 | 49 | ||
modified
internal/engine/engine.go +7 -0 | @@ -268,6 +268,9 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a | ||
| 268 | 268 | // `New` is called once per `Generate`, which keeps `full` captured at the |
| 269 | 269 | // right level. |
| 270 | 270 | var full []*ai.Message |
| 271 | + // reply is the model's latest message — for a tool, the one that called | |
| 272 | + // it (reply.go). | |
| 273 | + var reply *ai.Message | |
| 271 | 274 | // pl is the watchdog's pulse, set below; every model call restarts the |
| 272 | 275 | // wait for a first token (progress.go). |
| 273 | 276 | var pl *pulse |
| @@ -349,6 +352,9 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a | ||
| 349 | 352 | } |
| 350 | 353 | } |
| 351 | 354 | dropReasoning(resp) |
| 355 | + if resp != nil { | |
| 356 | + reply = resp.Message | |
| 357 | + } | |
| 352 | 358 | return resp, err |
| 353 | 359 | }, |
| 354 | 360 | // The tool is about to write to the screen: we forget the |
| @@ -359,6 +365,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a | ||
| 359 | 365 | // call, built-in and MCP, before the tool asks for consent. |
| 360 | 366 | WrapTool: func(ctx context.Context, params *ai.ToolParams, next ai.ToolNext) (*ai.MultipartToolResponse, error) { |
| 361 | 367 | gaps.drop() |
| 368 | + ctx = withReply(ctx, reply) | |
| 362 | 369 | if !e.hooks.Enabled() { |
| 363 | 370 | return next(ctx, params) |
| 364 | 371 | } |
| @@ -268,6 +268,9 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a | |||
| 268 | // `New` is called once per `Generate`, which keeps `full` captured at the | 268 | // `New` is called once per `Generate`, which keeps `full` captured at the |
| 269 | // right level. | 269 | // right level. |
| 270 | var full []*ai.Message | 270 | var full []*ai.Message |
| 271 | + // reply is the model's latest message — for a tool, the one that called | ||
| 272 | + // it (reply.go). | ||
| 273 | + var reply *ai.Message | ||
| 271 | // pl is the watchdog's pulse, set below; every model call restarts the | 274 | // pl is the watchdog's pulse, set below; every model call restarts the |
| 272 | // wait for a first token (progress.go). | 275 | // wait for a first token (progress.go). |
| 273 | var pl *pulse | 276 | var pl *pulse |
| @@ -349,6 +352,9 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a | |||
| 349 | } | 352 | } |
| 350 | } | 353 | } |
| 351 | dropReasoning(resp) | 354 | dropReasoning(resp) |
| 355 | + if resp != nil { | ||
| 356 | + reply = resp.Message | ||
| 357 | + } | ||
| 352 | return resp, err | 358 | return resp, err |
| 353 | }, | 359 | }, |
| 354 | // The tool is about to write to the screen: we forget the | 360 | // The tool is about to write to the screen: we forget the |
| @@ -359,6 +365,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a | |||
| 359 | // call, built-in and MCP, before the tool asks for consent. | 365 | // call, built-in and MCP, before the tool asks for consent. |
| 360 | WrapTool: func(ctx context.Context, params *ai.ToolParams, next ai.ToolNext) (*ai.MultipartToolResponse, error) { | 366 | WrapTool: func(ctx context.Context, params *ai.ToolParams, next ai.ToolNext) (*ai.MultipartToolResponse, error) { |
| 361 | gaps.drop() | 367 | gaps.drop() |
| 368 | + ctx = withReply(ctx, reply) | ||
| 362 | if !e.hooks.Enabled() { | 369 | if !e.hooks.Enabled() { |
| 363 | return next(ctx, params) | 370 | return next(ctx, params) |
| 364 | } | 371 | } |
modified
internal/engine/engine_test.go +25 -0 | @@ -437,6 +437,31 @@ func TestToolsSeeTheConversation(t *testing.T) { | ||
| 437 | 437 | } |
| 438 | 438 | } |
| 439 | 439 | |
| 440 | +// A tool reads the model message that called it — its text included, which | |
| 441 | +// is what save_answer saves when the model writes the answer beside the call | |
| 442 | +// (ticket 93). | |
| 443 | +func TestToolsSeeTheReplyThatCalledThem(t *testing.T) { | |
| 444 | + call := modelTurn(ai.NewTextPart("# Nushell\nfirst steps"), ai.NewToolRequestPart(&ai.ToolRequest{Name: "peek", Input: map[string]any{}})) | |
| 445 | + g, _ := fakeGenkit(t, call, modelTurn(ai.NewTextPart("saved"))) | |
| 446 | + var seen *ai.Message | |
| 447 | + genkit.DefineTool(g, "peek", "look at the reply", | |
| 448 | + func(tctx *ai.ToolContext, _ struct{}) (string, error) { | |
| 449 | + seen = Reply(tctx) | |
| 450 | + return "ok", nil | |
| 451 | + }) | |
| 452 | + e := testEngine(g) | |
| 453 | + e.Provider, e.Backend = registry["dmr"], Backend{Provider: "dmr"} | |
| 454 | + if _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("teach me")}, []ai.ToolRef{ai.ToolName("peek")}); err != nil { | |
| 455 | + t.Fatal(err) | |
| 456 | + } | |
| 457 | + if seen == nil || seen.Text() != "# Nushell\nfirst steps" { | |
| 458 | + t.Errorf("the tool saw reply %v", seen) | |
| 459 | + } | |
| 460 | + if Reply(context.Background()) != nil { | |
| 461 | + t.Error("Reply outside a tool call is not nil") | |
| 462 | + } | |
| 463 | +} | |
| 464 | + | |
| 440 | 465 | // A tool that beats the heartbeat keeps the turn alive past watchdogTimeout — |
| 441 | 466 | // ask_agent does it on every event of the agent it asked; a tool that does |
| 442 | 467 | // not is still cut (ticket 57). |
| @@ -437,6 +437,31 @@ func TestToolsSeeTheConversation(t *testing.T) { | |||
| 437 | } | 437 | } |
| 438 | } | 438 | } |
| 439 | 439 | ||
| 440 | +// A tool reads the model message that called it — its text included, which | ||
| 441 | +// is what save_answer saves when the model writes the answer beside the call | ||
| 442 | +// (ticket 93). | ||
| 443 | +func TestToolsSeeTheReplyThatCalledThem(t *testing.T) { | ||
| 444 | + call := modelTurn(ai.NewTextPart("# Nushell\nfirst steps"), ai.NewToolRequestPart(&ai.ToolRequest{Name: "peek", Input: map[string]any{}})) | ||
| 445 | + g, _ := fakeGenkit(t, call, modelTurn(ai.NewTextPart("saved"))) | ||
| 446 | + var seen *ai.Message | ||
| 447 | + genkit.DefineTool(g, "peek", "look at the reply", | ||
| 448 | + func(tctx *ai.ToolContext, _ struct{}) (string, error) { | ||
| 449 | + seen = Reply(tctx) | ||
| 450 | + return "ok", nil | ||
| 451 | + }) | ||
| 452 | + e := testEngine(g) | ||
| 453 | + e.Provider, e.Backend = registry["dmr"], Backend{Provider: "dmr"} | ||
| 454 | + if _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("teach me")}, []ai.ToolRef{ai.ToolName("peek")}); err != nil { | ||
| 455 | + t.Fatal(err) | ||
| 456 | + } | ||
| 457 | + if seen == nil || seen.Text() != "# Nushell\nfirst steps" { | ||
| 458 | + t.Errorf("the tool saw reply %v", seen) | ||
| 459 | + } | ||
| 460 | + if Reply(context.Background()) != nil { | ||
| 461 | + t.Error("Reply outside a tool call is not nil") | ||
| 462 | + } | ||
| 463 | +} | ||
| 464 | + | ||
| 440 | // A tool that beats the heartbeat keeps the turn alive past watchdogTimeout — | 465 | // A tool that beats the heartbeat keeps the turn alive past watchdogTimeout — |
| 441 | // ask_agent does it on every event of the agent it asked; a tool that does | 466 | // ask_agent does it on every event of the agent it asked; a tool that does |
| 442 | // not is still cut (ticket 57). | 467 | // not is still cut (ticket 57). |
added
internal/engine/reply.go +27 -0 | new file mode 100644 | ||
| @@ -0,0 +1,27 @@ | ||
| 1 | +package engine | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + | |
| 6 | + "github.com/firebase/genkit/go/ai" | |
| 7 | +) | |
| 8 | + | |
| 9 | +// replyKey is the context key of the model message a tool call came from. | |
| 10 | +type replyKey struct{} | |
| 11 | + | |
| 12 | +// withReply marks a tool call's ctx with the model message that made it. | |
| 13 | +// Generate's WrapTool hook sets it (ticket 93): save_answer saves the text | |
| 14 | +// of that message when the model wrote the answer and the call together. | |
| 15 | +func withReply(ctx context.Context, reply *ai.Message) context.Context { | |
| 16 | + return context.WithValue(ctx, replyKey{}, reply) | |
| 17 | +} | |
| 18 | + | |
| 19 | +// Reply returns the model message that made the tool call ctx belongs to — | |
| 20 | +// its text, when any, is what the model wrote beside the call. nil outside a | |
| 21 | +// tool call made by Generate. The earlier history is Conversation(ctx). | |
| 22 | +// | |
| 23 | +// if r := engine.Reply(ctx); r != nil && strings.TrimSpace(r.Text()) != "" { … } | |
| 24 | +func Reply(ctx context.Context) *ai.Message { | |
| 25 | + reply, _ := ctx.Value(replyKey{}).(*ai.Message) | |
| 26 | + return reply | |
| 27 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | +package engine | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + | ||
| 6 | + "github.com/firebase/genkit/go/ai" | ||
| 7 | +) | ||
| 8 | + | ||
| 9 | +// replyKey is the context key of the model message a tool call came from. | ||
| 10 | +type replyKey struct{} | ||
| 11 | + | ||
| 12 | +// withReply marks a tool call's ctx with the model message that made it. | ||
| 13 | +// Generate's WrapTool hook sets it (ticket 93): save_answer saves the text | ||
| 14 | +// of that message when the model wrote the answer and the call together. | ||
| 15 | +func withReply(ctx context.Context, reply *ai.Message) context.Context { | ||
| 16 | + return context.WithValue(ctx, replyKey{}, reply) | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +// Reply returns the model message that made the tool call ctx belongs to — | ||
| 20 | +// its text, when any, is what the model wrote beside the call. nil outside a | ||
| 21 | +// tool call made by Generate. The earlier history is Conversation(ctx). | ||
| 22 | +// | ||
| 23 | +// if r := engine.Reply(ctx); r != nil && strings.TrimSpace(r.Text()) != "" { … } | ||
| 24 | +func Reply(ctx context.Context) *ai.Message { | ||
| 25 | + reply, _ := ctx.Value(replyKey{}).(*ai.Message) | ||
| 26 | + return reply | ||
| 27 | +} | ||
added
internal/saved/answers.go +25 -0 | new file mode 100644 | ||
| @@ -0,0 +1,25 @@ | ||
| 1 | +package saved | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "strings" | |
| 5 | + | |
| 6 | + "github.com/firebase/genkit/go/ai" | |
| 7 | +) | |
| 8 | + | |
| 9 | +// Answers is the text of every model message that spoke to the user — | |
| 10 | +// tool-call-only messages have none — oldest first. /copy, /save and the | |
| 11 | +// save_answer tool all count answers this way. | |
| 12 | +// | |
| 13 | +// last := saved.Answers(history)[len(saved.Answers(history))-1] | |
| 14 | +func Answers(messages []*ai.Message) []string { | |
| 15 | + var out []string | |
| 16 | + for _, m := range messages { | |
| 17 | + if m == nil || m.Role != ai.RoleModel { | |
| 18 | + continue | |
| 19 | + } | |
| 20 | + if text := strings.TrimSpace(m.Text()); text != "" { | |
| 21 | + out = append(out, text) | |
| 22 | + } | |
| 23 | + } | |
| 24 | + return out | |
| 25 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | +package saved | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "strings" | ||
| 5 | + | ||
| 6 | + "github.com/firebase/genkit/go/ai" | ||
| 7 | +) | ||
| 8 | + | ||
| 9 | +// Answers is the text of every model message that spoke to the user — | ||
| 10 | +// tool-call-only messages have none — oldest first. /copy, /save and the | ||
| 11 | +// save_answer tool all count answers this way. | ||
| 12 | +// | ||
| 13 | +// last := saved.Answers(history)[len(saved.Answers(history))-1] | ||
| 14 | +func Answers(messages []*ai.Message) []string { | ||
| 15 | + var out []string | ||
| 16 | + for _, m := range messages { | ||
| 17 | + if m == nil || m.Role != ai.RoleModel { | ||
| 18 | + continue | ||
| 19 | + } | ||
| 20 | + if text := strings.TrimSpace(m.Text()); text != "" { | ||
| 21 | + out = append(out, text) | ||
| 22 | + } | ||
| 23 | + } | ||
| 24 | + return out | ||
| 25 | +} | ||
added
internal/saved/saved.go +118 -0 | new file mode 100644 | ||
| @@ -0,0 +1,118 @@ | ||
| 1 | +// Package saved writes an answer of the model to a file — what the /save | |
| 2 | +// command and the save_answer tool share (ticket 93). | |
| 3 | +// | |
| 4 | +// Both exist so that saving an answer costs nothing: the text the model | |
| 5 | +// already wrote is written as it is, the model never generates it a second | |
| 6 | +// time. Measured on Mellum2.1 12B Thinking (2026-10-17): asking the model to | |
| 7 | +// put a ~1,000-word tutorial in write_file cost 10,000–16,000 generated | |
| 8 | +// tokens, 5 to 8 minutes, because it drafts the whole text in its reasoning | |
| 9 | +// and then writes it again. | |
| 10 | +// | |
| 11 | +// An existing file is never overwritten: the caller says so and the user (or | |
| 12 | +// the model) picks another name. | |
| 13 | +package saved | |
| 14 | + | |
| 15 | +import ( | |
| 16 | + "errors" | |
| 17 | + "fmt" | |
| 18 | + "os" | |
| 19 | + "path/filepath" | |
| 20 | + "strconv" | |
| 21 | + "strings" | |
| 22 | +) | |
| 23 | + | |
| 24 | +// ErrExists is returned when the target file is already there. | |
| 25 | +var ErrExists = errors.New("the file already exists") | |
| 26 | + | |
| 27 | +// Write creates path with text, a trailing newline added when missing, and | |
| 28 | +// the missing parent directories. It refuses an empty text and an existing | |
| 29 | +// file (ErrExists), so a saved answer never replaces anything. | |
| 30 | +// | |
| 31 | +// n, err := saved.Write("notes/nushell.md", answer) | |
| 32 | +// // n: bytes written; errors.Is(err, saved.ErrExists) when it was there | |
| 33 | +func Write(path, text string) (int, error) { | |
| 34 | + if strings.TrimSpace(path) == "" { | |
| 35 | + return 0, errors.New("no file name given") | |
| 36 | + } | |
| 37 | + if strings.TrimSpace(text) == "" { | |
| 38 | + return 0, errors.New("the answer is empty: nothing to save") | |
| 39 | + } | |
| 40 | + if !strings.HasSuffix(text, "\n") { | |
| 41 | + text += "\n" | |
| 42 | + } | |
| 43 | + if dir := filepath.Dir(path); dir != "." { | |
| 44 | + if err := os.MkdirAll(dir, 0o755); err != nil { | |
| 45 | + return 0, err | |
| 46 | + } | |
| 47 | + } | |
| 48 | + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) | |
| 49 | + if errors.Is(err, os.ErrExist) { | |
| 50 | + return 0, fmt.Errorf("%s: %w", path, ErrExists) | |
| 51 | + } | |
| 52 | + if err != nil { | |
| 53 | + return 0, err | |
| 54 | + } | |
| 55 | + n, err := f.WriteString(text) | |
| 56 | + if closeErr := f.Close(); err == nil { | |
| 57 | + err = closeErr | |
| 58 | + } | |
| 59 | + return n, err | |
| 60 | +} | |
| 61 | + | |
| 62 | +// ParseArg reads /save's argument: a file name, then optionally which answer | |
| 63 | +// counting from the end (1 = the last one, the default). | |
| 64 | +// | |
| 65 | +// ParseArg("nushell.md") // "nushell.md", 1, nil | |
| 66 | +// ParseArg("nushell.md 2") // "nushell.md", 2, nil | |
| 67 | +func ParseArg(arg string) (path string, n int, err error) { | |
| 68 | + fields := strings.Fields(arg) | |
| 69 | + switch len(fields) { | |
| 70 | + case 0: | |
| 71 | + return "", 0, errors.New("/save takes a file name: /save notes.md (or /save notes.md 2 for the answer before)") | |
| 72 | + case 1: | |
| 73 | + return fields[0], 1, nil | |
| 74 | + case 2: | |
| 75 | + n, err := strconv.Atoi(fields[1]) | |
| 76 | + if err != nil || n < 1 { | |
| 77 | + return "", 0, fmt.Errorf("/save: %q is not an answer number (1 = the last answer)", fields[1]) | |
| 78 | + } | |
| 79 | + return fields[0], n, nil | |
| 80 | + } | |
| 81 | + return "", 0, errors.New("/save takes a file name and, optionally, an answer number — a name with spaces is not supported") | |
| 82 | +} | |
| 83 | + | |
| 84 | +// Pick returns the n-th answer from the end of answers (1 = the last). | |
| 85 | +func Pick(answers []string, n int) (string, error) { | |
| 86 | + if n > len(answers) { | |
| 87 | + return "", fmt.Errorf("nothing to save: %d answer(s) in the conversation, %d asked for", len(answers), n) | |
| 88 | + } | |
| 89 | + return answers[len(answers)-n], nil | |
| 90 | +} | |
| 91 | + | |
| 92 | +// Command runs /save over a conversation's answers and returns the line to | |
| 93 | +// show: what was written, or why nothing was. | |
| 94 | +// | |
| 95 | +// fmt.Println(saved.Command(answers, "nushell.md")) | |
| 96 | +// // 💾 saved the last answer to nushell.md (4211 bytes) | |
| 97 | +func Command(answers []string, arg string) string { | |
| 98 | + path, n, err := ParseArg(arg) | |
| 99 | + if err != nil { | |
| 100 | + return "⚠️ " + err.Error() | |
| 101 | + } | |
| 102 | + text, err := Pick(answers, n) | |
| 103 | + if err != nil { | |
| 104 | + return "⚠️ " + err.Error() | |
| 105 | + } | |
| 106 | + written, err := Write(path, text) | |
| 107 | + if errors.Is(err, ErrExists) { | |
| 108 | + return fmt.Sprintf("⚠️ %s already exists — nothing was written; choose another name", path) | |
| 109 | + } | |
| 110 | + if err != nil { | |
| 111 | + return "⚠️ /save: " + err.Error() | |
| 112 | + } | |
| 113 | + which := "the last answer" | |
| 114 | + if n > 1 { | |
| 115 | + which = fmt.Sprintf("answer %d from the end", n) | |
| 116 | + } | |
| 117 | + return fmt.Sprintf("💾 saved %s to %s (%d bytes)", which, path, written) | |
| 118 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | +// Package saved writes an answer of the model to a file — what the /save | ||
| 2 | +// command and the save_answer tool share (ticket 93). | ||
| 3 | +// | ||
| 4 | +// Both exist so that saving an answer costs nothing: the text the model | ||
| 5 | +// already wrote is written as it is, the model never generates it a second | ||
| 6 | +// time. Measured on Mellum2.1 12B Thinking (2026-10-17): asking the model to | ||
| 7 | +// put a ~1,000-word tutorial in write_file cost 10,000–16,000 generated | ||
| 8 | +// tokens, 5 to 8 minutes, because it drafts the whole text in its reasoning | ||
| 9 | +// and then writes it again. | ||
| 10 | +// | ||
| 11 | +// An existing file is never overwritten: the caller says so and the user (or | ||
| 12 | +// the model) picks another name. | ||
| 13 | +package saved | ||
| 14 | + | ||
| 15 | +import ( | ||
| 16 | + "errors" | ||
| 17 | + "fmt" | ||
| 18 | + "os" | ||
| 19 | + "path/filepath" | ||
| 20 | + "strconv" | ||
| 21 | + "strings" | ||
| 22 | +) | ||
| 23 | + | ||
| 24 | +// ErrExists is returned when the target file is already there. | ||
| 25 | +var ErrExists = errors.New("the file already exists") | ||
| 26 | + | ||
| 27 | +// Write creates path with text, a trailing newline added when missing, and | ||
| 28 | +// the missing parent directories. It refuses an empty text and an existing | ||
| 29 | +// file (ErrExists), so a saved answer never replaces anything. | ||
| 30 | +// | ||
| 31 | +// n, err := saved.Write("notes/nushell.md", answer) | ||
| 32 | +// // n: bytes written; errors.Is(err, saved.ErrExists) when it was there | ||
| 33 | +func Write(path, text string) (int, error) { | ||
| 34 | + if strings.TrimSpace(path) == "" { | ||
| 35 | + return 0, errors.New("no file name given") | ||
| 36 | + } | ||
| 37 | + if strings.TrimSpace(text) == "" { | ||
| 38 | + return 0, errors.New("the answer is empty: nothing to save") | ||
| 39 | + } | ||
| 40 | + if !strings.HasSuffix(text, "\n") { | ||
| 41 | + text += "\n" | ||
| 42 | + } | ||
| 43 | + if dir := filepath.Dir(path); dir != "." { | ||
| 44 | + if err := os.MkdirAll(dir, 0o755); err != nil { | ||
| 45 | + return 0, err | ||
| 46 | + } | ||
| 47 | + } | ||
| 48 | + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) | ||
| 49 | + if errors.Is(err, os.ErrExist) { | ||
| 50 | + return 0, fmt.Errorf("%s: %w", path, ErrExists) | ||
| 51 | + } | ||
| 52 | + if err != nil { | ||
| 53 | + return 0, err | ||
| 54 | + } | ||
| 55 | + n, err := f.WriteString(text) | ||
| 56 | + if closeErr := f.Close(); err == nil { | ||
| 57 | + err = closeErr | ||
| 58 | + } | ||
| 59 | + return n, err | ||
| 60 | +} | ||
| 61 | + | ||
| 62 | +// ParseArg reads /save's argument: a file name, then optionally which answer | ||
| 63 | +// counting from the end (1 = the last one, the default). | ||
| 64 | +// | ||
| 65 | +// ParseArg("nushell.md") // "nushell.md", 1, nil | ||
| 66 | +// ParseArg("nushell.md 2") // "nushell.md", 2, nil | ||
| 67 | +func ParseArg(arg string) (path string, n int, err error) { | ||
| 68 | + fields := strings.Fields(arg) | ||
| 69 | + switch len(fields) { | ||
| 70 | + case 0: | ||
| 71 | + return "", 0, errors.New("/save takes a file name: /save notes.md (or /save notes.md 2 for the answer before)") | ||
| 72 | + case 1: | ||
| 73 | + return fields[0], 1, nil | ||
| 74 | + case 2: | ||
| 75 | + n, err := strconv.Atoi(fields[1]) | ||
| 76 | + if err != nil || n < 1 { | ||
| 77 | + return "", 0, fmt.Errorf("/save: %q is not an answer number (1 = the last answer)", fields[1]) | ||
| 78 | + } | ||
| 79 | + return fields[0], n, nil | ||
| 80 | + } | ||
| 81 | + return "", 0, errors.New("/save takes a file name and, optionally, an answer number — a name with spaces is not supported") | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +// Pick returns the n-th answer from the end of answers (1 = the last). | ||
| 85 | +func Pick(answers []string, n int) (string, error) { | ||
| 86 | + if n > len(answers) { | ||
| 87 | + return "", fmt.Errorf("nothing to save: %d answer(s) in the conversation, %d asked for", len(answers), n) | ||
| 88 | + } | ||
| 89 | + return answers[len(answers)-n], nil | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +// Command runs /save over a conversation's answers and returns the line to | ||
| 93 | +// show: what was written, or why nothing was. | ||
| 94 | +// | ||
| 95 | +// fmt.Println(saved.Command(answers, "nushell.md")) | ||
| 96 | +// // 💾 saved the last answer to nushell.md (4211 bytes) | ||
| 97 | +func Command(answers []string, arg string) string { | ||
| 98 | + path, n, err := ParseArg(arg) | ||
| 99 | + if err != nil { | ||
| 100 | + return "⚠️ " + err.Error() | ||
| 101 | + } | ||
| 102 | + text, err := Pick(answers, n) | ||
| 103 | + if err != nil { | ||
| 104 | + return "⚠️ " + err.Error() | ||
| 105 | + } | ||
| 106 | + written, err := Write(path, text) | ||
| 107 | + if errors.Is(err, ErrExists) { | ||
| 108 | + return fmt.Sprintf("⚠️ %s already exists — nothing was written; choose another name", path) | ||
| 109 | + } | ||
| 110 | + if err != nil { | ||
| 111 | + return "⚠️ /save: " + err.Error() | ||
| 112 | + } | ||
| 113 | + which := "the last answer" | ||
| 114 | + if n > 1 { | ||
| 115 | + which = fmt.Sprintf("answer %d from the end", n) | ||
| 116 | + } | ||
| 117 | + return fmt.Sprintf("💾 saved %s to %s (%d bytes)", which, path, written) | ||
| 118 | +} | ||
added
internal/saved/saved_test.go +83 -0 | new file mode 100644 | ||
| @@ -0,0 +1,83 @@ | ||
| 1 | +package saved | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "errors" | |
| 5 | + "os" | |
| 6 | + "path/filepath" | |
| 7 | + "strings" | |
| 8 | + "testing" | |
| 9 | +) | |
| 10 | + | |
| 11 | +// Write creates the file and its directories, adds the final newline, and | |
| 12 | +// never replaces an existing file. | |
| 13 | +func TestWrite(t *testing.T) { | |
| 14 | + dir := t.TempDir() | |
| 15 | + path := filepath.Join(dir, "notes", "nu.md") | |
| 16 | + n, err := Write(path, "# Nu") | |
| 17 | + if err != nil || n != len("# Nu\n") { | |
| 18 | + t.Fatalf("Write = %d, %v", n, err) | |
| 19 | + } | |
| 20 | + if b, _ := os.ReadFile(path); string(b) != "# Nu\n" { | |
| 21 | + t.Errorf("file = %q", b) | |
| 22 | + } | |
| 23 | + if _, err := Write(path, "other"); !errors.Is(err, ErrExists) { | |
| 24 | + t.Errorf("second Write = %v, want ErrExists", err) | |
| 25 | + } | |
| 26 | + if b, _ := os.ReadFile(path); string(b) != "# Nu\n" { | |
| 27 | + t.Errorf("the existing file was changed: %q", b) | |
| 28 | + } | |
| 29 | + if _, err := Write(filepath.Join(dir, "x.md"), " \n"); err == nil { | |
| 30 | + t.Error("an empty answer was saved") | |
| 31 | + } | |
| 32 | + if _, err := Write(" ", "text"); err == nil { | |
| 33 | + t.Error("a blank name was accepted") | |
| 34 | + } | |
| 35 | +} | |
| 36 | + | |
| 37 | +func TestParseArg(t *testing.T) { | |
| 38 | + cases := []struct { | |
| 39 | + arg string | |
| 40 | + path string | |
| 41 | + n int | |
| 42 | + ok bool | |
| 43 | + }{ | |
| 44 | + {"nu.md", "nu.md", 1, true}, | |
| 45 | + {" nu.md 2 ", "nu.md", 2, true}, | |
| 46 | + {"", "", 0, false}, | |
| 47 | + {"nu.md zero", "", 0, false}, | |
| 48 | + {"nu.md 0", "", 0, false}, | |
| 49 | + {"my notes.md 2", "", 0, false}, | |
| 50 | + } | |
| 51 | + for _, c := range cases { | |
| 52 | + path, n, err := ParseArg(c.arg) | |
| 53 | + if (err == nil) != c.ok || path != c.path || n != c.n { | |
| 54 | + t.Errorf("ParseArg(%q) = %q, %d, %v", c.arg, path, n, err) | |
| 55 | + } | |
| 56 | + } | |
| 57 | +} | |
| 58 | + | |
| 59 | +// Command picks the answer, writes it, and says what happened in one line. | |
| 60 | +func TestCommand(t *testing.T) { | |
| 61 | + dir := t.TempDir() | |
| 62 | + answers := []string{"first", "second"} | |
| 63 | + path := filepath.Join(dir, "a.md") | |
| 64 | + | |
| 65 | + if got := Command(answers, path); !strings.HasPrefix(got, "💾 saved the last answer to ") { | |
| 66 | + t.Errorf("save last = %q", got) | |
| 67 | + } | |
| 68 | + if b, _ := os.ReadFile(path); string(b) != "second\n" { | |
| 69 | + t.Errorf("file = %q", b) | |
| 70 | + } | |
| 71 | + if got := Command(answers, filepath.Join(dir, "b.md")+" 2"); !strings.Contains(got, "answer 2 from the end") { | |
| 72 | + t.Errorf("save 2 = %q", got) | |
| 73 | + } | |
| 74 | + if got := Command(answers, path); !strings.Contains(got, "already exists — nothing was written") { | |
| 75 | + t.Errorf("existing = %q", got) | |
| 76 | + } | |
| 77 | + if got := Command(answers, filepath.Join(dir, "c.md")+" 3"); !strings.Contains(got, "2 answer(s) in the conversation") { | |
| 78 | + t.Errorf("too far = %q", got) | |
| 79 | + } | |
| 80 | + if got := Command(nil, ""); !strings.Contains(got, "/save takes a file name") { | |
| 81 | + t.Errorf("no arg = %q", got) | |
| 82 | + } | |
| 83 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | +package saved | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "errors" | ||
| 5 | + "os" | ||
| 6 | + "path/filepath" | ||
| 7 | + "strings" | ||
| 8 | + "testing" | ||
| 9 | +) | ||
| 10 | + | ||
| 11 | +// Write creates the file and its directories, adds the final newline, and | ||
| 12 | +// never replaces an existing file. | ||
| 13 | +func TestWrite(t *testing.T) { | ||
| 14 | + dir := t.TempDir() | ||
| 15 | + path := filepath.Join(dir, "notes", "nu.md") | ||
| 16 | + n, err := Write(path, "# Nu") | ||
| 17 | + if err != nil || n != len("# Nu\n") { | ||
| 18 | + t.Fatalf("Write = %d, %v", n, err) | ||
| 19 | + } | ||
| 20 | + if b, _ := os.ReadFile(path); string(b) != "# Nu\n" { | ||
| 21 | + t.Errorf("file = %q", b) | ||
| 22 | + } | ||
| 23 | + if _, err := Write(path, "other"); !errors.Is(err, ErrExists) { | ||
| 24 | + t.Errorf("second Write = %v, want ErrExists", err) | ||
| 25 | + } | ||
| 26 | + if b, _ := os.ReadFile(path); string(b) != "# Nu\n" { | ||
| 27 | + t.Errorf("the existing file was changed: %q", b) | ||
| 28 | + } | ||
| 29 | + if _, err := Write(filepath.Join(dir, "x.md"), " \n"); err == nil { | ||
| 30 | + t.Error("an empty answer was saved") | ||
| 31 | + } | ||
| 32 | + if _, err := Write(" ", "text"); err == nil { | ||
| 33 | + t.Error("a blank name was accepted") | ||
| 34 | + } | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +func TestParseArg(t *testing.T) { | ||
| 38 | + cases := []struct { | ||
| 39 | + arg string | ||
| 40 | + path string | ||
| 41 | + n int | ||
| 42 | + ok bool | ||
| 43 | + }{ | ||
| 44 | + {"nu.md", "nu.md", 1, true}, | ||
| 45 | + {" nu.md 2 ", "nu.md", 2, true}, | ||
| 46 | + {"", "", 0, false}, | ||
| 47 | + {"nu.md zero", "", 0, false}, | ||
| 48 | + {"nu.md 0", "", 0, false}, | ||
| 49 | + {"my notes.md 2", "", 0, false}, | ||
| 50 | + } | ||
| 51 | + for _, c := range cases { | ||
| 52 | + path, n, err := ParseArg(c.arg) | ||
| 53 | + if (err == nil) != c.ok || path != c.path || n != c.n { | ||
| 54 | + t.Errorf("ParseArg(%q) = %q, %d, %v", c.arg, path, n, err) | ||
| 55 | + } | ||
| 56 | + } | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +// Command picks the answer, writes it, and says what happened in one line. | ||
| 60 | +func TestCommand(t *testing.T) { | ||
| 61 | + dir := t.TempDir() | ||
| 62 | + answers := []string{"first", "second"} | ||
| 63 | + path := filepath.Join(dir, "a.md") | ||
| 64 | + | ||
| 65 | + if got := Command(answers, path); !strings.HasPrefix(got, "💾 saved the last answer to ") { | ||
| 66 | + t.Errorf("save last = %q", got) | ||
| 67 | + } | ||
| 68 | + if b, _ := os.ReadFile(path); string(b) != "second\n" { | ||
| 69 | + t.Errorf("file = %q", b) | ||
| 70 | + } | ||
| 71 | + if got := Command(answers, filepath.Join(dir, "b.md")+" 2"); !strings.Contains(got, "answer 2 from the end") { | ||
| 72 | + t.Errorf("save 2 = %q", got) | ||
| 73 | + } | ||
| 74 | + if got := Command(answers, path); !strings.Contains(got, "already exists — nothing was written") { | ||
| 75 | + t.Errorf("existing = %q", got) | ||
| 76 | + } | ||
| 77 | + if got := Command(answers, filepath.Join(dir, "c.md")+" 3"); !strings.Contains(got, "2 answer(s) in the conversation") { | ||
| 78 | + t.Errorf("too far = %q", got) | ||
| 79 | + } | ||
| 80 | + if got := Command(nil, ""); !strings.Contains(got, "/save takes a file name") { | ||
| 81 | + t.Errorf("no arg = %q", got) | ||
| 82 | + } | ||
| 83 | +} | ||
modified
internal/tools/plan_actions.go +42 -0 | @@ -3,6 +3,7 @@ package tools | ||
| 3 | 3 | import ( |
| 4 | 4 | "encoding/json" |
| 5 | 5 | "fmt" |
| 6 | + "slices" | |
| 6 | 7 | "strings" |
| 7 | 8 | ) |
| 8 | 9 | |
| @@ -61,7 +62,14 @@ func planGet(in planInput) string { | ||
| 61 | 62 | return planJSON(PlanOutput{Result: msg, Plan: plan}) |
| 62 | 63 | } |
| 63 | 64 | |
| 65 | +// planStatuses are the values `update` accepts, in the order they are listed | |
| 66 | +// to the model. | |
| 67 | +var planStatuses = []string{"open", "in_progress", "done", "failed"} | |
| 68 | + | |
| 64 | 69 | func planUpdate(in planInput) string { |
| 70 | + if !slices.Contains(planStatuses, in.Status) { | |
| 71 | + return invalidUpdate(in) | |
| 72 | + } | |
| 65 | 73 | _, msg, err := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message) |
| 66 | 74 | if err != nil { |
| 67 | 75 | return planError(err) |
| @@ -70,6 +78,40 @@ func planUpdate(in planInput) string { | ||
| 70 | 78 | return fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine) |
| 71 | 79 | } |
| 72 | 80 | |
| 81 | +// invalidUpdate answers an `update` without a valid status (ticket 91). | |
| 82 | +// | |
| 83 | +// The store's own message for a missing status — "invalid status" followed by | |
| 84 | +// an empty quoted value and the four allowed ones — named neither the missing | |
| 85 | +// field nor what `update` is for. Mellum2.1 12B sent | |
| 86 | +// {"action":"update","taskId":1,"tasks":[…]} about thirty times in a row, | |
| 87 | +// trying to edit the plan's tasks, until maxTurns ended the turn; its calls | |
| 88 | +// differed slightly each time, so the loop detector never fired. The answer | |
| 89 | +// therefore quotes the call back, names the field, says that `tasks` is not | |
| 90 | +// read here and how to change tasks, and gives a call that works. | |
| 91 | +func invalidUpdate(in planInput) string { | |
| 92 | + problem := fmt.Sprintf("%q is not a status", in.Status) | |
| 93 | + if in.Status == "" { | |
| 94 | + problem = `"status" is missing` | |
| 95 | + } | |
| 96 | + name := in.Name | |
| 97 | + if name == "" { | |
| 98 | + name = "user-request-1" | |
| 99 | + } | |
| 100 | + taskID := in.TaskID | |
| 101 | + if taskID <= 0 { | |
| 102 | + taskID = 1 | |
| 103 | + } | |
| 104 | + sent, _ := json.Marshal(in) | |
| 105 | + var b strings.Builder | |
| 106 | + fmt.Fprintf(&b, "[error: plan update changes the status of ONE task, and %s. You sent: %s. ", problem, sent) | |
| 107 | + fmt.Fprintf(&b, `"status" must be one of: %s. `, strings.Join(planStatuses, ", ")) | |
| 108 | + if len(in.Tasks) > 0 { | |
| 109 | + b.WriteString(`"tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again. `) | |
| 110 | + } | |
| 111 | + fmt.Fprintf(&b, `A valid call: {"action":"update","name":%q,"taskId":%d,"status":"done"}]`, name, taskID) | |
| 112 | + return b.String() | |
| 113 | +} | |
| 114 | + | |
| 73 | 115 | func planNext(in planInput) string { |
| 74 | 116 | _, nextTask, msg, err := planStore.NextTask(in.Name) |
| 75 | 117 | if err != nil { |
| @@ -3,6 +3,7 @@ package tools | |||
| 3 | import ( | 3 | import ( |
| 4 | "encoding/json" | 4 | "encoding/json" |
| 5 | "fmt" | 5 | "fmt" |
| 6 | + "slices" | ||
| 6 | "strings" | 7 | "strings" |
| 7 | ) | 8 | ) |
| 8 | 9 | ||
| @@ -61,7 +62,14 @@ func planGet(in planInput) string { | |||
| 61 | return planJSON(PlanOutput{Result: msg, Plan: plan}) | 62 | return planJSON(PlanOutput{Result: msg, Plan: plan}) |
| 62 | } | 63 | } |
| 63 | 64 | ||
| 65 | +// planStatuses are the values `update` accepts, in the order they are listed | ||
| 66 | +// to the model. | ||
| 67 | +var planStatuses = []string{"open", "in_progress", "done", "failed"} | ||
| 68 | + | ||
| 64 | func planUpdate(in planInput) string { | 69 | func planUpdate(in planInput) string { |
| 70 | + if !slices.Contains(planStatuses, in.Status) { | ||
| 71 | + return invalidUpdate(in) | ||
| 72 | + } | ||
| 65 | _, msg, err := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message) | 73 | _, msg, err := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message) |
| 66 | if err != nil { | 74 | if err != nil { |
| 67 | return planError(err) | 75 | return planError(err) |
| @@ -70,6 +78,40 @@ func planUpdate(in planInput) string { | |||
| 70 | return fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine) | 78 | return fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine) |
| 71 | } | 79 | } |
| 72 | 80 | ||
| 81 | +// invalidUpdate answers an `update` without a valid status (ticket 91). | ||
| 82 | +// | ||
| 83 | +// The store's own message for a missing status — "invalid status" followed by | ||
| 84 | +// an empty quoted value and the four allowed ones — named neither the missing | ||
| 85 | +// field nor what `update` is for. Mellum2.1 12B sent | ||
| 86 | +// {"action":"update","taskId":1,"tasks":[…]} about thirty times in a row, | ||
| 87 | +// trying to edit the plan's tasks, until maxTurns ended the turn; its calls | ||
| 88 | +// differed slightly each time, so the loop detector never fired. The answer | ||
| 89 | +// therefore quotes the call back, names the field, says that `tasks` is not | ||
| 90 | +// read here and how to change tasks, and gives a call that works. | ||
| 91 | +func invalidUpdate(in planInput) string { | ||
| 92 | + problem := fmt.Sprintf("%q is not a status", in.Status) | ||
| 93 | + if in.Status == "" { | ||
| 94 | + problem = `"status" is missing` | ||
| 95 | + } | ||
| 96 | + name := in.Name | ||
| 97 | + if name == "" { | ||
| 98 | + name = "user-request-1" | ||
| 99 | + } | ||
| 100 | + taskID := in.TaskID | ||
| 101 | + if taskID <= 0 { | ||
| 102 | + taskID = 1 | ||
| 103 | + } | ||
| 104 | + sent, _ := json.Marshal(in) | ||
| 105 | + var b strings.Builder | ||
| 106 | + fmt.Fprintf(&b, "[error: plan update changes the status of ONE task, and %s. You sent: %s. ", problem, sent) | ||
| 107 | + fmt.Fprintf(&b, `"status" must be one of: %s. `, strings.Join(planStatuses, ", ")) | ||
| 108 | + if len(in.Tasks) > 0 { | ||
| 109 | + b.WriteString(`"tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again. `) | ||
| 110 | + } | ||
| 111 | + fmt.Fprintf(&b, `A valid call: {"action":"update","name":%q,"taskId":%d,"status":"done"}]`, name, taskID) | ||
| 112 | + return b.String() | ||
| 113 | +} | ||
| 114 | + | ||
| 73 | func planNext(in planInput) string { | 115 | func planNext(in planInput) string { |
| 74 | _, nextTask, msg, err := planStore.NextTask(in.Name) | 116 | _, nextTask, msg, err := planStore.NextTask(in.Name) |
| 75 | if err != nil { | 117 | if err != nil { |
modified
internal/tools/plan_actions_test.go +42 -0 | @@ -41,3 +41,45 @@ func TestRunPlanAction(t *testing.T) { | ||
| 41 | 41 | } |
| 42 | 42 | } |
| 43 | 43 | } |
| 44 | + | |
| 45 | +// The call Mellum2.1 repeated about thirty times (ticket 91): an update with | |
| 46 | +// tasks and no status. The answer names the missing field, quotes the call, | |
| 47 | +// says tasks are not read by update, and gives a call that works — and the | |
| 48 | +// plan is left untouched. | |
| 49 | +func TestPlanUpdateWithoutStatusExplainsTheCall(t *testing.T) { | |
| 50 | + ClearAllPlansForTesting() | |
| 51 | + runPlanAction(planInput{Action: "create", Name: "user-request-1", Tasks: []PlanTaskInput{{Description: "Intro"}}}, true) | |
| 52 | + | |
| 53 | + got := runPlanAction(planInput{ | |
| 54 | + Action: "update", Name: "user-request-1", TaskID: 1, | |
| 55 | + Tasks: []PlanTaskInput{{ID: 1, Description: "Write introduction in French about Nu shell"}}, | |
| 56 | + }, true) | |
| 57 | + for _, want := range []string{ | |
| 58 | + `plan update changes the status of ONE task, and "status" is missing`, | |
| 59 | + `You sent: {"action":"update","name":"user-request-1","tasks":[{"id":1,"description":"Write introduction in French about Nu shell"}],"taskId":1}`, | |
| 60 | + `"status" must be one of: open, in_progress, done, failed`, | |
| 61 | + `"tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again`, | |
| 62 | + `A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}`, | |
| 63 | + } { | |
| 64 | + if !strings.Contains(got, want) { | |
| 65 | + t.Errorf("answer %s\nlacks %s", got, want) | |
| 66 | + } | |
| 67 | + } | |
| 68 | + if status := runPlanAction(planInput{Action: "status", Name: "user-request-1"}, true); !strings.Contains(status, "Task 1: Intro (open)") { | |
| 69 | + t.Errorf("the refused update changed the plan: %s", status) | |
| 70 | + } | |
| 71 | +} | |
| 72 | + | |
| 73 | +// A status that is not one of the four is refused the same way, without the | |
| 74 | +// sentence about tasks when none were sent, and with defaults in the example | |
| 75 | +// when the call gave no name or task. | |
| 76 | +func TestPlanUpdateWithAnUnknownStatus(t *testing.T) { | |
| 77 | + ClearAllPlansForTesting() | |
| 78 | + got := runPlanAction(planInput{Action: "update", Status: "finished"}, true) | |
| 79 | + if !strings.Contains(got, `and "finished" is not a status`) || strings.Contains(got, `"tasks" is ignored`) { | |
| 80 | + t.Errorf("answer %s", got) | |
| 81 | + } | |
| 82 | + if !strings.Contains(got, `A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}`) { | |
| 83 | + t.Errorf("answer %s lacks the default example", got) | |
| 84 | + } | |
| 85 | +} | |
| @@ -41,3 +41,45 @@ func TestRunPlanAction(t *testing.T) { | |||
| 41 | } | 41 | } |
| 42 | } | 42 | } |
| 43 | } | 43 | } |
| 44 | + | ||
| 45 | +// The call Mellum2.1 repeated about thirty times (ticket 91): an update with | ||
| 46 | +// tasks and no status. The answer names the missing field, quotes the call, | ||
| 47 | +// says tasks are not read by update, and gives a call that works — and the | ||
| 48 | +// plan is left untouched. | ||
| 49 | +func TestPlanUpdateWithoutStatusExplainsTheCall(t *testing.T) { | ||
| 50 | + ClearAllPlansForTesting() | ||
| 51 | + runPlanAction(planInput{Action: "create", Name: "user-request-1", Tasks: []PlanTaskInput{{Description: "Intro"}}}, true) | ||
| 52 | + | ||
| 53 | + got := runPlanAction(planInput{ | ||
| 54 | + Action: "update", Name: "user-request-1", TaskID: 1, | ||
| 55 | + Tasks: []PlanTaskInput{{ID: 1, Description: "Write introduction in French about Nu shell"}}, | ||
| 56 | + }, true) | ||
| 57 | + for _, want := range []string{ | ||
| 58 | + `plan update changes the status of ONE task, and "status" is missing`, | ||
| 59 | + `You sent: {"action":"update","name":"user-request-1","tasks":[{"id":1,"description":"Write introduction in French about Nu shell"}],"taskId":1}`, | ||
| 60 | + `"status" must be one of: open, in_progress, done, failed`, | ||
| 61 | + `"tasks" is ignored by update (only create reads it): to change a plan's tasks, delete the plan and create it again`, | ||
| 62 | + `A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}`, | ||
| 63 | + } { | ||
| 64 | + if !strings.Contains(got, want) { | ||
| 65 | + t.Errorf("answer %s\nlacks %s", got, want) | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + if status := runPlanAction(planInput{Action: "status", Name: "user-request-1"}, true); !strings.Contains(status, "Task 1: Intro (open)") { | ||
| 69 | + t.Errorf("the refused update changed the plan: %s", status) | ||
| 70 | + } | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +// A status that is not one of the four is refused the same way, without the | ||
| 74 | +// sentence about tasks when none were sent, and with defaults in the example | ||
| 75 | +// when the call gave no name or task. | ||
| 76 | +func TestPlanUpdateWithAnUnknownStatus(t *testing.T) { | ||
| 77 | + ClearAllPlansForTesting() | ||
| 78 | + got := runPlanAction(planInput{Action: "update", Status: "finished"}, true) | ||
| 79 | + if !strings.Contains(got, `and "finished" is not a status`) || strings.Contains(got, `"tasks" is ignored`) { | ||
| 80 | + t.Errorf("answer %s", got) | ||
| 81 | + } | ||
| 82 | + if !strings.Contains(got, `A valid call: {"action":"update","name":"user-request-1","taskId":1,"status":"done"}`) { | ||
| 83 | + t.Errorf("answer %s lacks the default example", got) | ||
| 84 | + } | ||
| 85 | +} | ||
added
internal/tools/saveanswer.go +117 -0 | new file mode 100644 | ||
| @@ -0,0 +1,117 @@ | ||
| 1 | +package tools | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "errors" | |
| 6 | + "fmt" | |
| 7 | + "strings" | |
| 8 | + | |
| 9 | + "mm/internal/consent" | |
| 10 | + "mm/internal/detector" | |
| 11 | + "mm/internal/engine" | |
| 12 | + "mm/internal/saved" | |
| 13 | + "mm/internal/spinner" | |
| 14 | + "mm/internal/ui" | |
| 15 | + | |
| 16 | + "github.com/firebase/genkit/go/ai" | |
| 17 | + "github.com/firebase/genkit/go/genkit" | |
| 18 | +) | |
| 19 | + | |
| 20 | +// --- save_answer (ticket 93) ------------------------------------------------- | |
| 21 | +// | |
| 22 | +// Writes an answer the model ALREADY gave to a file: the model sends a file | |
| 23 | +// name, mm writes the text. Saving through write_file makes the model | |
| 24 | +// generate the whole text again — measured on Mellum2.1 12B Thinking, 10,000 | |
| 25 | +// to 16,000 tokens and 5 to 8 minutes for a ~1,000-word tutorial (see | |
| 26 | +// internal/saved). | |
| 27 | +// | |
| 28 | +// Which answer: the text the model wrote in the same reply as the call, when | |
| 29 | +// there is one ("here is the tutorial … [save_answer]"); otherwise its | |
| 30 | +// previous answer ("save it in nushell.md" after the tutorial was given). | |
| 31 | +// An existing file is never overwritten. | |
| 32 | + | |
| 33 | +type saveAnswerInput struct { | |
| 34 | + Path string `json:"path" jsonschema_description:"The file to create, relative to the working directory, e.g. nushell-first-script.md. It must not exist yet."` | |
| 35 | +} | |
| 36 | + | |
| 37 | +// SaveAnswer declares the `save_answer` tool (config saveAnswerTool). | |
| 38 | +// | |
| 39 | +// set.tools = append(set.tools, tools.SaveAnswer(e.G, d)) | |
| 40 | +func SaveAnswer(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef { | |
| 41 | + description := `Save an answer you already gave to a Markdown file, as it is: you only give the file name, you do NOT write the text again. | |
| 42 | +It saves the text of your CURRENT reply when it has one; otherwise your PREVIOUS answer. So, when the user asks to save an answer you gave before, call this tool alone, with no text in your reply. | |
| 43 | +Use it only when the user asks to save, write or export an answer to a file. It never overwrites a file: if the name is taken, it says so — pick another name or ask the user.` | |
| 44 | + | |
| 45 | + return genkit.DefineTool(g, "save_answer", description, | |
| 46 | + func(tctx *ai.ToolContext, in saveAnswerInput) (string, error) { | |
| 47 | + text, which := answerToSave(engine.Reply(tctx), engine.Conversation(tctx)) | |
| 48 | + if s := ui.ActiveSink(); s != nil { | |
| 49 | + return saveAnswerEvented(tctx, s, d, in, text, which) | |
| 50 | + } | |
| 51 | + title := fmt.Sprintf("save_answer: %s (%s)", in.Path, which) | |
| 52 | + announce("💾 " + title) | |
| 53 | + if text != "" && !confirmed(tctx, "save_answer", title) { | |
| 54 | + return refuse(d, "save_answer", in.Path, "[save_answer rejected by the user]") | |
| 55 | + } | |
| 56 | + out, _ := saveAnswer(ui.Resolve(in.Path), in.Path, text, which) | |
| 57 | + spinner.Suspend(func() { echoOutput(out) }) | |
| 58 | + return record(d, "save_answer", in.Path, out) | |
| 59 | + }) | |
| 60 | +} | |
| 61 | + | |
| 62 | +// saveAnswerEvented is save_answer for an event-consuming front end (ACP, | |
| 63 | +// the window): it writes a file, so it asks first when yolo is off. | |
| 64 | +func saveAnswerEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in saveAnswerInput, text, which string) (string, error) { | |
| 65 | + path := ui.Resolve(in.Path) | |
| 66 | + ev := ui.ToolEvent{ | |
| 67 | + ID: ui.NextCallID(), | |
| 68 | + Tool: "save_answer", | |
| 69 | + Title: fmt.Sprintf("save_answer: %s (%s)", in.Path, which), | |
| 70 | + Kind: "edit", | |
| 71 | + Input: map[string]any{"path": in.Path}, | |
| 72 | + Path: absolute(path), | |
| 73 | + } | |
| 74 | + s.ToolStart(ev) | |
| 75 | + if text != "" && !consent.Gate(ctx, s, ev) { | |
| 76 | + out := "[save_answer rejected by the user]" | |
| 77 | + s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | |
| 78 | + return record(d, "save_answer", in.Path, out) | |
| 79 | + } | |
| 80 | + s.ToolRunning(ev.ID) | |
| 81 | + out, failed := saveAnswer(path, in.Path, text, which) | |
| 82 | + s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: failed}) | |
| 83 | + return record(d, "save_answer", in.Path, out) | |
| 84 | +} | |
| 85 | + | |
| 86 | +// answerToSave picks the text: the reply carrying the call when it has text, | |
| 87 | +// else the last answer of the history. which names the choice for the | |
| 88 | +// display line and the result. | |
| 89 | +func answerToSave(reply *ai.Message, history []*ai.Message) (text, which string) { | |
| 90 | + if reply != nil { | |
| 91 | + if t := strings.TrimSpace(reply.Text()); t != "" { | |
| 92 | + return t, "this answer" | |
| 93 | + } | |
| 94 | + } | |
| 95 | + answers := saved.Answers(history) | |
| 96 | + if len(answers) == 0 { | |
| 97 | + return "", "no answer yet" | |
| 98 | + } | |
| 99 | + return answers[len(answers)-1], "the previous answer" | |
| 100 | +} | |
| 101 | + | |
| 102 | +// saveAnswer writes text to path (shown as name) and returns the result the | |
| 103 | +// model reads, and whether it failed. | |
| 104 | +func saveAnswer(path, name, text, which string) (string, bool) { | |
| 105 | + if text == "" { | |
| 106 | + return "[save_answer: there is no answer to save yet — write the answer in your reply first, then call save_answer with the file name]", true | |
| 107 | + } | |
| 108 | + n, err := saved.Write(path, text) | |
| 109 | + if errors.Is(err, saved.ErrExists) { | |
| 110 | + return fmt.Sprintf("[save_answer: %s already exists — nothing was written. Pick another name, or ask the user]", name), true | |
| 111 | + } | |
| 112 | + if err != nil { | |
| 113 | + return fmt.Sprintf("[save_answer: %v]", err), true | |
| 114 | + } | |
| 115 | + first, _, _ := strings.Cut(text, "\n") | |
| 116 | + return fmt.Sprintf("saved %s to %s (%d bytes), starting with: %s", which, name, n, clip(first)), false | |
| 117 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,117 @@ | |||
| 1 | +package tools | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "errors" | ||
| 6 | + "fmt" | ||
| 7 | + "strings" | ||
| 8 | + | ||
| 9 | + "mm/internal/consent" | ||
| 10 | + "mm/internal/detector" | ||
| 11 | + "mm/internal/engine" | ||
| 12 | + "mm/internal/saved" | ||
| 13 | + "mm/internal/spinner" | ||
| 14 | + "mm/internal/ui" | ||
| 15 | + | ||
| 16 | + "github.com/firebase/genkit/go/ai" | ||
| 17 | + "github.com/firebase/genkit/go/genkit" | ||
| 18 | +) | ||
| 19 | + | ||
| 20 | +// --- save_answer (ticket 93) ------------------------------------------------- | ||
| 21 | +// | ||
| 22 | +// Writes an answer the model ALREADY gave to a file: the model sends a file | ||
| 23 | +// name, mm writes the text. Saving through write_file makes the model | ||
| 24 | +// generate the whole text again — measured on Mellum2.1 12B Thinking, 10,000 | ||
| 25 | +// to 16,000 tokens and 5 to 8 minutes for a ~1,000-word tutorial (see | ||
| 26 | +// internal/saved). | ||
| 27 | +// | ||
| 28 | +// Which answer: the text the model wrote in the same reply as the call, when | ||
| 29 | +// there is one ("here is the tutorial … [save_answer]"); otherwise its | ||
| 30 | +// previous answer ("save it in nushell.md" after the tutorial was given). | ||
| 31 | +// An existing file is never overwritten. | ||
| 32 | + | ||
| 33 | +type saveAnswerInput struct { | ||
| 34 | + Path string `json:"path" jsonschema_description:"The file to create, relative to the working directory, e.g. nushell-first-script.md. It must not exist yet."` | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +// SaveAnswer declares the `save_answer` tool (config saveAnswerTool). | ||
| 38 | +// | ||
| 39 | +// set.tools = append(set.tools, tools.SaveAnswer(e.G, d)) | ||
| 40 | +func SaveAnswer(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef { | ||
| 41 | + description := `Save an answer you already gave to a Markdown file, as it is: you only give the file name, you do NOT write the text again. | ||
| 42 | +It saves the text of your CURRENT reply when it has one; otherwise your PREVIOUS answer. So, when the user asks to save an answer you gave before, call this tool alone, with no text in your reply. | ||
| 43 | +Use it only when the user asks to save, write or export an answer to a file. It never overwrites a file: if the name is taken, it says so — pick another name or ask the user.` | ||
| 44 | + | ||
| 45 | + return genkit.DefineTool(g, "save_answer", description, | ||
| 46 | + func(tctx *ai.ToolContext, in saveAnswerInput) (string, error) { | ||
| 47 | + text, which := answerToSave(engine.Reply(tctx), engine.Conversation(tctx)) | ||
| 48 | + if s := ui.ActiveSink(); s != nil { | ||
| 49 | + return saveAnswerEvented(tctx, s, d, in, text, which) | ||
| 50 | + } | ||
| 51 | + title := fmt.Sprintf("save_answer: %s (%s)", in.Path, which) | ||
| 52 | + announce("💾 " + title) | ||
| 53 | + if text != "" && !confirmed(tctx, "save_answer", title) { | ||
| 54 | + return refuse(d, "save_answer", in.Path, "[save_answer rejected by the user]") | ||
| 55 | + } | ||
| 56 | + out, _ := saveAnswer(ui.Resolve(in.Path), in.Path, text, which) | ||
| 57 | + spinner.Suspend(func() { echoOutput(out) }) | ||
| 58 | + return record(d, "save_answer", in.Path, out) | ||
| 59 | + }) | ||
| 60 | +} | ||
| 61 | + | ||
| 62 | +// saveAnswerEvented is save_answer for an event-consuming front end (ACP, | ||
| 63 | +// the window): it writes a file, so it asks first when yolo is off. | ||
| 64 | +func saveAnswerEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in saveAnswerInput, text, which string) (string, error) { | ||
| 65 | + path := ui.Resolve(in.Path) | ||
| 66 | + ev := ui.ToolEvent{ | ||
| 67 | + ID: ui.NextCallID(), | ||
| 68 | + Tool: "save_answer", | ||
| 69 | + Title: fmt.Sprintf("save_answer: %s (%s)", in.Path, which), | ||
| 70 | + Kind: "edit", | ||
| 71 | + Input: map[string]any{"path": in.Path}, | ||
| 72 | + Path: absolute(path), | ||
| 73 | + } | ||
| 74 | + s.ToolStart(ev) | ||
| 75 | + if text != "" && !consent.Gate(ctx, s, ev) { | ||
| 76 | + out := "[save_answer rejected by the user]" | ||
| 77 | + s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true}) | ||
| 78 | + return record(d, "save_answer", in.Path, out) | ||
| 79 | + } | ||
| 80 | + s.ToolRunning(ev.ID) | ||
| 81 | + out, failed := saveAnswer(path, in.Path, text, which) | ||
| 82 | + s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: failed}) | ||
| 83 | + return record(d, "save_answer", in.Path, out) | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +// answerToSave picks the text: the reply carrying the call when it has text, | ||
| 87 | +// else the last answer of the history. which names the choice for the | ||
| 88 | +// display line and the result. | ||
| 89 | +func answerToSave(reply *ai.Message, history []*ai.Message) (text, which string) { | ||
| 90 | + if reply != nil { | ||
| 91 | + if t := strings.TrimSpace(reply.Text()); t != "" { | ||
| 92 | + return t, "this answer" | ||
| 93 | + } | ||
| 94 | + } | ||
| 95 | + answers := saved.Answers(history) | ||
| 96 | + if len(answers) == 0 { | ||
| 97 | + return "", "no answer yet" | ||
| 98 | + } | ||
| 99 | + return answers[len(answers)-1], "the previous answer" | ||
| 100 | +} | ||
| 101 | + | ||
| 102 | +// saveAnswer writes text to path (shown as name) and returns the result the | ||
| 103 | +// model reads, and whether it failed. | ||
| 104 | +func saveAnswer(path, name, text, which string) (string, bool) { | ||
| 105 | + if text == "" { | ||
| 106 | + return "[save_answer: there is no answer to save yet — write the answer in your reply first, then call save_answer with the file name]", true | ||
| 107 | + } | ||
| 108 | + n, err := saved.Write(path, text) | ||
| 109 | + if errors.Is(err, saved.ErrExists) { | ||
| 110 | + return fmt.Sprintf("[save_answer: %s already exists — nothing was written. Pick another name, or ask the user]", name), true | ||
| 111 | + } | ||
| 112 | + if err != nil { | ||
| 113 | + return fmt.Sprintf("[save_answer: %v]", err), true | ||
| 114 | + } | ||
| 115 | + first, _, _ := strings.Cut(text, "\n") | ||
| 116 | + return fmt.Sprintf("saved %s to %s (%d bytes), starting with: %s", which, name, n, clip(first)), false | ||
| 117 | +} | ||
added
internal/tools/saveanswer_test.go +106 -0 | new file mode 100644 | ||
| @@ -0,0 +1,106 @@ | ||
| 1 | +package tools | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "os" | |
| 6 | + "path/filepath" | |
| 7 | + "strings" | |
| 8 | + "testing" | |
| 9 | + | |
| 10 | + "mm/internal/detector" | |
| 11 | + "mm/internal/ui" | |
| 12 | + | |
| 13 | + "github.com/firebase/genkit/go/ai" | |
| 14 | +) | |
| 15 | + | |
| 16 | +// inSessionDir makes sink the active sink for the test: ui.Resolve then | |
| 17 | +// resolves paths in the sink's directory, as in an editor session. | |
| 18 | +func inSessionDir(t *testing.T, sink *recordingSink) *recordingSink { | |
| 19 | + t.Helper() | |
| 20 | + ui.SetActive(sink) | |
| 21 | + t.Cleanup(func() { ui.SetActive(nil) }) | |
| 22 | + return sink | |
| 23 | +} | |
| 24 | + | |
| 25 | +// The text beside the call wins; a call alone saves the previous answer; | |
| 26 | +// with no answer at all there is nothing to save. | |
| 27 | +func TestAnswerToSave(t *testing.T) { | |
| 28 | + history := []*ai.Message{ | |
| 29 | + ai.NewUserTextMessage("teach me nushell"), | |
| 30 | + ai.NewModelTextMessage("# Nushell\nfirst steps"), | |
| 31 | + ai.NewUserTextMessage("save it in nu.md"), | |
| 32 | + } | |
| 33 | + if text, which := answerToSave(ai.NewModelTextMessage(" # Here\n "), history); text != "# Here" || which != "this answer" { | |
| 34 | + t.Errorf("with text in the reply: %q, %q", text, which) | |
| 35 | + } | |
| 36 | + if text, which := answerToSave(&ai.Message{Role: ai.RoleModel}, history); text != "# Nushell\nfirst steps" || which != "the previous answer" { | |
| 37 | + t.Errorf("call alone: %q, %q", text, which) | |
| 38 | + } | |
| 39 | + if text, which := answerToSave(nil, history[:1]); text != "" || which != "no answer yet" { | |
| 40 | + t.Errorf("no answer: %q, %q", text, which) | |
| 41 | + } | |
| 42 | +} | |
| 43 | + | |
| 44 | +// With yolo on the answer is written at once, in the session's directory; | |
| 45 | +// the result names the choice and quotes the first line. | |
| 46 | +func TestSaveAnswerWritesTheAnswer(t *testing.T) { | |
| 47 | + dir := t.TempDir() | |
| 48 | + sink := inSessionDir(t, &recordingSink{dir: dir}) | |
| 49 | + out, err := saveAnswerEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), | |
| 50 | + saveAnswerInput{Path: "nu.md"}, "# Nushell\nfirst steps", "the previous answer") | |
| 51 | + if err != nil || sink.asked != 0 || sink.ended.Failed { | |
| 52 | + t.Fatalf("out %q, err %v, asked %d, ended %+v", out, err, sink.asked, sink.ended) | |
| 53 | + } | |
| 54 | + if !strings.HasPrefix(out, "saved the previous answer to nu.md (") || !strings.HasSuffix(out, "starting with: # Nushell") { | |
| 55 | + t.Errorf("result %q", out) | |
| 56 | + } | |
| 57 | + if b, _ := os.ReadFile(filepath.Join(dir, "nu.md")); string(b) != "# Nushell\nfirst steps\n" { | |
| 58 | + t.Errorf("file %q", b) | |
| 59 | + } | |
| 60 | + if sink.started.Kind != "edit" || sink.started.Path != filepath.Join(dir, "nu.md") { | |
| 61 | + t.Errorf("event %+v", sink.started) | |
| 62 | + } | |
| 63 | +} | |
| 64 | + | |
| 65 | +// An existing file is refused and left as it was; with no answer nothing is | |
| 66 | +// asked and nothing is written. | |
| 67 | +func TestSaveAnswerRefusals(t *testing.T) { | |
| 68 | + dir := t.TempDir() | |
| 69 | + if err := os.WriteFile(filepath.Join(dir, "nu.md"), []byte("keep me"), 0o644); err != nil { | |
| 70 | + t.Fatal(err) | |
| 71 | + } | |
| 72 | + sink := inSessionDir(t, &recordingSink{dir: dir}) | |
| 73 | + d := detector.NewLoopDetector(10, 3) | |
| 74 | + out, _ := saveAnswerEvented(context.Background(), sink, d, saveAnswerInput{Path: "nu.md"}, "new text", "this answer") | |
| 75 | + if !strings.Contains(out, "nu.md already exists — nothing was written") || !sink.ended.Failed { | |
| 76 | + t.Errorf("existing: %q", out) | |
| 77 | + } | |
| 78 | + if b, _ := os.ReadFile(filepath.Join(dir, "nu.md")); string(b) != "keep me" { | |
| 79 | + t.Errorf("the existing file changed: %q", b) | |
| 80 | + } | |
| 81 | + | |
| 82 | + yoloOff(t) | |
| 83 | + sink = inSessionDir(t, &recordingSink{dir: dir, allow: true}) | |
| 84 | + out, _ = saveAnswerEvented(context.Background(), sink, d, saveAnswerInput{Path: "empty.md"}, "", "no answer yet") | |
| 85 | + if sink.asked != 0 || !strings.Contains(out, "there is no answer to save yet") { | |
| 86 | + t.Errorf("no answer: asked %d, %q", sink.asked, out) | |
| 87 | + } | |
| 88 | + if _, err := os.Stat(filepath.Join(dir, "empty.md")); !os.IsNotExist(err) { | |
| 89 | + t.Error("a file was written with no answer") | |
| 90 | + } | |
| 91 | +} | |
| 92 | + | |
| 93 | +// With yolo off the write waits for the user, and a refusal writes nothing. | |
| 94 | +func TestSaveAnswerAsksWhenYoloIsOff(t *testing.T) { | |
| 95 | + yoloOff(t) | |
| 96 | + dir := t.TempDir() | |
| 97 | + sink := inSessionDir(t, &recordingSink{dir: dir, allow: false}) | |
| 98 | + out, _ := saveAnswerEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), | |
| 99 | + saveAnswerInput{Path: "nu.md"}, "text", "this answer") | |
| 100 | + if sink.asked != 1 || out != "[save_answer rejected by the user]" { | |
| 101 | + t.Errorf("asked %d, out %q", sink.asked, out) | |
| 102 | + } | |
| 103 | + if _, err := os.Stat(filepath.Join(dir, "nu.md")); !os.IsNotExist(err) { | |
| 104 | + t.Error("a refused save wrote the file") | |
| 105 | + } | |
| 106 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,106 @@ | |||
| 1 | +package tools | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "os" | ||
| 6 | + "path/filepath" | ||
| 7 | + "strings" | ||
| 8 | + "testing" | ||
| 9 | + | ||
| 10 | + "mm/internal/detector" | ||
| 11 | + "mm/internal/ui" | ||
| 12 | + | ||
| 13 | + "github.com/firebase/genkit/go/ai" | ||
| 14 | +) | ||
| 15 | + | ||
| 16 | +// inSessionDir makes sink the active sink for the test: ui.Resolve then | ||
| 17 | +// resolves paths in the sink's directory, as in an editor session. | ||
| 18 | +func inSessionDir(t *testing.T, sink *recordingSink) *recordingSink { | ||
| 19 | + t.Helper() | ||
| 20 | + ui.SetActive(sink) | ||
| 21 | + t.Cleanup(func() { ui.SetActive(nil) }) | ||
| 22 | + return sink | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +// The text beside the call wins; a call alone saves the previous answer; | ||
| 26 | +// with no answer at all there is nothing to save. | ||
| 27 | +func TestAnswerToSave(t *testing.T) { | ||
| 28 | + history := []*ai.Message{ | ||
| 29 | + ai.NewUserTextMessage("teach me nushell"), | ||
| 30 | + ai.NewModelTextMessage("# Nushell\nfirst steps"), | ||
| 31 | + ai.NewUserTextMessage("save it in nu.md"), | ||
| 32 | + } | ||
| 33 | + if text, which := answerToSave(ai.NewModelTextMessage(" # Here\n "), history); text != "# Here" || which != "this answer" { | ||
| 34 | + t.Errorf("with text in the reply: %q, %q", text, which) | ||
| 35 | + } | ||
| 36 | + if text, which := answerToSave(&ai.Message{Role: ai.RoleModel}, history); text != "# Nushell\nfirst steps" || which != "the previous answer" { | ||
| 37 | + t.Errorf("call alone: %q, %q", text, which) | ||
| 38 | + } | ||
| 39 | + if text, which := answerToSave(nil, history[:1]); text != "" || which != "no answer yet" { | ||
| 40 | + t.Errorf("no answer: %q, %q", text, which) | ||
| 41 | + } | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +// With yolo on the answer is written at once, in the session's directory; | ||
| 45 | +// the result names the choice and quotes the first line. | ||
| 46 | +func TestSaveAnswerWritesTheAnswer(t *testing.T) { | ||
| 47 | + dir := t.TempDir() | ||
| 48 | + sink := inSessionDir(t, &recordingSink{dir: dir}) | ||
| 49 | + out, err := saveAnswerEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), | ||
| 50 | + saveAnswerInput{Path: "nu.md"}, "# Nushell\nfirst steps", "the previous answer") | ||
| 51 | + if err != nil || sink.asked != 0 || sink.ended.Failed { | ||
| 52 | + t.Fatalf("out %q, err %v, asked %d, ended %+v", out, err, sink.asked, sink.ended) | ||
| 53 | + } | ||
| 54 | + if !strings.HasPrefix(out, "saved the previous answer to nu.md (") || !strings.HasSuffix(out, "starting with: # Nushell") { | ||
| 55 | + t.Errorf("result %q", out) | ||
| 56 | + } | ||
| 57 | + if b, _ := os.ReadFile(filepath.Join(dir, "nu.md")); string(b) != "# Nushell\nfirst steps\n" { | ||
| 58 | + t.Errorf("file %q", b) | ||
| 59 | + } | ||
| 60 | + if sink.started.Kind != "edit" || sink.started.Path != filepath.Join(dir, "nu.md") { | ||
| 61 | + t.Errorf("event %+v", sink.started) | ||
| 62 | + } | ||
| 63 | +} | ||
| 64 | + | ||
| 65 | +// An existing file is refused and left as it was; with no answer nothing is | ||
| 66 | +// asked and nothing is written. | ||
| 67 | +func TestSaveAnswerRefusals(t *testing.T) { | ||
| 68 | + dir := t.TempDir() | ||
| 69 | + if err := os.WriteFile(filepath.Join(dir, "nu.md"), []byte("keep me"), 0o644); err != nil { | ||
| 70 | + t.Fatal(err) | ||
| 71 | + } | ||
| 72 | + sink := inSessionDir(t, &recordingSink{dir: dir}) | ||
| 73 | + d := detector.NewLoopDetector(10, 3) | ||
| 74 | + out, _ := saveAnswerEvented(context.Background(), sink, d, saveAnswerInput{Path: "nu.md"}, "new text", "this answer") | ||
| 75 | + if !strings.Contains(out, "nu.md already exists — nothing was written") || !sink.ended.Failed { | ||
| 76 | + t.Errorf("existing: %q", out) | ||
| 77 | + } | ||
| 78 | + if b, _ := os.ReadFile(filepath.Join(dir, "nu.md")); string(b) != "keep me" { | ||
| 79 | + t.Errorf("the existing file changed: %q", b) | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + yoloOff(t) | ||
| 83 | + sink = inSessionDir(t, &recordingSink{dir: dir, allow: true}) | ||
| 84 | + out, _ = saveAnswerEvented(context.Background(), sink, d, saveAnswerInput{Path: "empty.md"}, "", "no answer yet") | ||
| 85 | + if sink.asked != 0 || !strings.Contains(out, "there is no answer to save yet") { | ||
| 86 | + t.Errorf("no answer: asked %d, %q", sink.asked, out) | ||
| 87 | + } | ||
| 88 | + if _, err := os.Stat(filepath.Join(dir, "empty.md")); !os.IsNotExist(err) { | ||
| 89 | + t.Error("a file was written with no answer") | ||
| 90 | + } | ||
| 91 | +} | ||
| 92 | + | ||
| 93 | +// With yolo off the write waits for the user, and a refusal writes nothing. | ||
| 94 | +func TestSaveAnswerAsksWhenYoloIsOff(t *testing.T) { | ||
| 95 | + yoloOff(t) | ||
| 96 | + dir := t.TempDir() | ||
| 97 | + sink := inSessionDir(t, &recordingSink{dir: dir, allow: false}) | ||
| 98 | + out, _ := saveAnswerEvented(context.Background(), sink, detector.NewLoopDetector(10, 3), | ||
| 99 | + saveAnswerInput{Path: "nu.md"}, "text", "this answer") | ||
| 100 | + if sink.asked != 1 || out != "[save_answer rejected by the user]" { | ||
| 101 | + t.Errorf("asked %d, out %q", sink.asked, out) | ||
| 102 | + } | ||
| 103 | + if _, err := os.Stat(filepath.Join(dir, "nu.md")); !os.IsNotExist(err) { | ||
| 104 | + t.Error("a refused save wrote the file") | ||
| 105 | + } | ||
| 106 | +} | ||
modified
internal/tui/commands.go +2 -0 | @@ -99,6 +99,8 @@ func (r *runner) dispatch(p Prompt) { | ||
| 99 | 99 | r.help() |
| 100 | 100 | case command.Copy: |
| 101 | 101 | r.copy(arg) |
| 102 | + case command.Save: | |
| 103 | + r.saveAnswer(arg) | |
| 102 | 104 | } |
| 103 | 105 | } |
| 104 | 106 | |
| @@ -99,6 +99,8 @@ func (r *runner) dispatch(p Prompt) { | |||
| 99 | r.help() | 99 | r.help() |
| 100 | case command.Copy: | 100 | case command.Copy: |
| 101 | r.copy(arg) | 101 | r.copy(arg) |
| 102 | + case command.Save: | ||
| 103 | + r.saveAnswer(arg) | ||
| 102 | } | 104 | } |
| 103 | } | 105 | } |
| 104 | 106 | ||
modified
internal/tui/copy.go +13 -0 | @@ -3,8 +3,10 @@ package tui | ||
| 3 | 3 | import ( |
| 4 | 4 | "fmt" |
| 5 | 5 | "strconv" |
| 6 | + "strings" | |
| 6 | 7 | |
| 7 | 8 | "mm/internal/clipboard" |
| 9 | + "mm/internal/saved" | |
| 8 | 10 | ) |
| 9 | 11 | |
| 10 | 12 | // /copy — the last answer, or the whole conversation, onto the clipboard. |
| @@ -56,3 +58,14 @@ func copyText(t *Transcript, arg string) (string, error) { | ||
| 56 | 58 | } |
| 57 | 59 | return answers[len(answers)-n], nil |
| 58 | 60 | } |
| 61 | + | |
| 62 | +// saveAnswer is /save <file> [n]: the last answer (or the n-th from the end) | |
| 63 | +// written as it is to a new file — never over an existing one. | |
| 64 | +func (r *runner) saveAnswer(arg string) { | |
| 65 | + line := saved.Command(r.t.Answers(), arg) | |
| 66 | + if strings.HasPrefix(line, "⚠️") { | |
| 67 | + r.t.Error(line) | |
| 68 | + return | |
| 69 | + } | |
| 70 | + r.t.Notice(line) | |
| 71 | +} | |
| @@ -3,8 +3,10 @@ package tui | |||
| 3 | import ( | 3 | import ( |
| 4 | "fmt" | 4 | "fmt" |
| 5 | "strconv" | 5 | "strconv" |
| 6 | + "strings" | ||
| 6 | 7 | ||
| 7 | "mm/internal/clipboard" | 8 | "mm/internal/clipboard" |
| 9 | + "mm/internal/saved" | ||
| 8 | ) | 10 | ) |
| 9 | 11 | ||
| 10 | // /copy — the last answer, or the whole conversation, onto the clipboard. | 12 | // /copy — the last answer, or the whole conversation, onto the clipboard. |
| @@ -56,3 +58,14 @@ func copyText(t *Transcript, arg string) (string, error) { | |||
| 56 | } | 58 | } |
| 57 | return answers[len(answers)-n], nil | 59 | return answers[len(answers)-n], nil |
| 58 | } | 60 | } |
| 61 | + | ||
| 62 | +// saveAnswer is /save <file> [n]: the last answer (or the n-th from the end) | ||
| 63 | +// written as it is to a new file — never over an existing one. | ||
| 64 | +func (r *runner) saveAnswer(arg string) { | ||
| 65 | + line := saved.Command(r.t.Answers(), arg) | ||
| 66 | + if strings.HasPrefix(line, "⚠️") { | ||
| 67 | + r.t.Error(line) | ||
| 68 | + return | ||
| 69 | + } | ||
| 70 | + r.t.Notice(line) | ||
| 71 | +} | ||
modified
internal/tui/turn_test.go +21 -0 | @@ -369,6 +369,27 @@ func TestRunnerCopy(t *testing.T) { | ||
| 369 | 369 | } |
| 370 | 370 | } |
| 371 | 371 | |
| 372 | +// /save writes the raw last answer to a new file and says so; an existing | |
| 373 | +// file is left alone, and the refusal is an error notice. | |
| 374 | +func TestRunnerSave(t *testing.T) { | |
| 375 | + r, tr := newTestRunner(t, &fakeModel{answer: "forty-two"}) | |
| 376 | + r.dispatch(promptOf("what is the answer?")) | |
| 377 | + waitIdle(t, r) | |
| 378 | + | |
| 379 | + path := filepath.Join(t.TempDir(), "answer.md") | |
| 380 | + r.dispatch(promptOf("/save " + path)) | |
| 381 | + if got := notices(tr); !strings.Contains(got, "💾 saved the last answer to "+path) { | |
| 382 | + t.Fatalf("no confirmation:\n%s", got) | |
| 383 | + } | |
| 384 | + if b, _ := os.ReadFile(path); string(b) != "forty-two\n" { | |
| 385 | + t.Errorf("file = %q, want the raw answer", b) | |
| 386 | + } | |
| 387 | + r.dispatch(promptOf("/save " + path)) | |
| 388 | + if got := notices(tr); !strings.Contains(got, "already exists — nothing was written") { | |
| 389 | + t.Errorf("an existing file was not refused:\n%s", got) | |
| 390 | + } | |
| 391 | +} | |
| 392 | + | |
| 372 | 393 | // A long paste reaches the model in full, shows collapsed in the conversation, |
| 373 | 394 | // and its @paths are not looked up — only the ones the user typed are. |
| 374 | 395 | func TestRunnerTurnWithPaste(t *testing.T) { |
| @@ -369,6 +369,27 @@ func TestRunnerCopy(t *testing.T) { | |||
| 369 | } | 369 | } |
| 370 | } | 370 | } |
| 371 | 371 | ||
| 372 | +// /save writes the raw last answer to a new file and says so; an existing | ||
| 373 | +// file is left alone, and the refusal is an error notice. | ||
| 374 | +func TestRunnerSave(t *testing.T) { | ||
| 375 | + r, tr := newTestRunner(t, &fakeModel{answer: "forty-two"}) | ||
| 376 | + r.dispatch(promptOf("what is the answer?")) | ||
| 377 | + waitIdle(t, r) | ||
| 378 | + | ||
| 379 | + path := filepath.Join(t.TempDir(), "answer.md") | ||
| 380 | + r.dispatch(promptOf("/save " + path)) | ||
| 381 | + if got := notices(tr); !strings.Contains(got, "💾 saved the last answer to "+path) { | ||
| 382 | + t.Fatalf("no confirmation:\n%s", got) | ||
| 383 | + } | ||
| 384 | + if b, _ := os.ReadFile(path); string(b) != "forty-two\n" { | ||
| 385 | + t.Errorf("file = %q, want the raw answer", b) | ||
| 386 | + } | ||
| 387 | + r.dispatch(promptOf("/save " + path)) | ||
| 388 | + if got := notices(tr); !strings.Contains(got, "already exists — nothing was written") { | ||
| 389 | + t.Errorf("an existing file was not refused:\n%s", got) | ||
| 390 | + } | ||
| 391 | +} | ||
| 392 | + | ||
| 372 | // A long paste reaches the model in full, shows collapsed in the conversation, | 393 | // A long paste reaches the model in full, shows collapsed in the conversation, |
| 373 | // and its @paths are not looked up — only the ones the user typed are. | 394 | // and its @paths are not looked up — only the ones the user typed are. |
| 374 | func TestRunnerTurnWithPaste(t *testing.T) { | 395 | func TestRunnerTurnWithPaste(t *testing.T) { |
modified
internal/tui/view_test.go +2 -2 | @@ -163,8 +163,8 @@ func TestPasteNeverSends(t *testing.T) { | ||
| 163 | 163 | func TestPicker(t *testing.T) { |
| 164 | 164 | hs := newHarness(t, 70, 20) |
| 165 | 165 | hs.typeText("/") |
| 166 | - if got := len(hs.v.choices()); got != 8 { | |
| 167 | - t.Fatalf("%d choices on '/', want 8", got) | |
| 166 | + if got := len(hs.v.choices()); got != 9 { | |
| 167 | + t.Fatalf("%d choices on '/', want 9", got) | |
| 168 | 168 | } |
| 169 | 169 | if text := hs.screenText(); !strings.Contains(text, "/resume") || !strings.Contains(text, "Continue a stored session") { |
| 170 | 170 | t.Errorf("picker not drawn:\n%s", text) |
| @@ -163,8 +163,8 @@ func TestPasteNeverSends(t *testing.T) { | |||
| 163 | func TestPicker(t *testing.T) { | 163 | func TestPicker(t *testing.T) { |
| 164 | hs := newHarness(t, 70, 20) | 164 | hs := newHarness(t, 70, 20) |
| 165 | hs.typeText("/") | 165 | hs.typeText("/") |
| 166 | - if got := len(hs.v.choices()); got != 8 { | 166 | + if got := len(hs.v.choices()); got != 9 { |
| 167 | - t.Fatalf("%d choices on '/', want 8", got) | 167 | + t.Fatalf("%d choices on '/', want 9", got) |
| 168 | } | 168 | } |
| 169 | if text := hs.screenText(); !strings.Contains(text, "/resume") || !strings.Contains(text, "Continue a stored session") { | 169 | if text := hs.screenText(); !strings.Contains(text, "/resume") || !strings.Contains(text, "Continue a stored session") { |
| 170 | t.Errorf("picker not drawn:\n%s", text) | 170 | t.Errorf("picker not drawn:\n%s", text) |
modified
main.go +6 -1 | @@ -337,7 +337,8 @@ type toolSet struct { | ||
| 337 | 337 | // the PATH — a missing server is a warning, never a refusal to start; and |
| 338 | 338 | // `ask_agent` when mmConfigs names at least one agent, over the same pool as |
| 339 | 339 | // /ask; `ask_user` behind `askUserTool` (on by default), never with a |
| 340 | -// structuredOutput; and the agent file's declared `tools:`. | |
| 340 | +// structuredOutput; `save_answer` behind `saveAnswerTool` (off by default); | |
| 341 | +// and the agent file's declared `tools:`. | |
| 341 | 342 | func assembleTools(e *engine.Engine, d *detector.LoopDetector, asker tools.AgentAsker) toolSet { |
| 342 | 343 | set := toolSet{close: func() {}} |
| 343 | 344 | if config.Cfg.BashTool { |
| @@ -373,6 +374,10 @@ func assembleTools(e *engine.Engine, d *detector.LoopDetector, asker tools.Agent | ||
| 373 | 374 | set.tools = append(set.tools, tools.AskUser(e.G, d)) |
| 374 | 375 | set.names = append(set.names, "ask_user") |
| 375 | 376 | } |
| 377 | + if config.Cfg.SaveAnswerTool { | |
| 378 | + set.tools = append(set.tools, tools.SaveAnswer(e.G, d)) | |
| 379 | + set.names = append(set.names, "save_answer") | |
| 380 | + } | |
| 376 | 381 | if config.Cfg.FetchContentTool { |
| 377 | 382 | // The product token "mm" is what the sites' robots.txt are read for. |
| 378 | 383 | fetcher := webmd.NewFetcher("mm/" + version.Short()) |
| @@ -337,7 +337,8 @@ type toolSet struct { | |||
| 337 | // the PATH — a missing server is a warning, never a refusal to start; and | 337 | // the PATH — a missing server is a warning, never a refusal to start; and |
| 338 | // `ask_agent` when mmConfigs names at least one agent, over the same pool as | 338 | // `ask_agent` when mmConfigs names at least one agent, over the same pool as |
| 339 | // /ask; `ask_user` behind `askUserTool` (on by default), never with a | 339 | // /ask; `ask_user` behind `askUserTool` (on by default), never with a |
| 340 | -// structuredOutput; and the agent file's declared `tools:`. | 340 | +// structuredOutput; `save_answer` behind `saveAnswerTool` (off by default); |
| 341 | +// and the agent file's declared `tools:`. | ||
| 341 | func assembleTools(e *engine.Engine, d *detector.LoopDetector, asker tools.AgentAsker) toolSet { | 342 | func assembleTools(e *engine.Engine, d *detector.LoopDetector, asker tools.AgentAsker) toolSet { |
| 342 | set := toolSet{close: func() {}} | 343 | set := toolSet{close: func() {}} |
| 343 | if config.Cfg.BashTool { | 344 | if config.Cfg.BashTool { |
| @@ -373,6 +374,10 @@ func assembleTools(e *engine.Engine, d *detector.LoopDetector, asker tools.Agent | |||
| 373 | set.tools = append(set.tools, tools.AskUser(e.G, d)) | 374 | set.tools = append(set.tools, tools.AskUser(e.G, d)) |
| 374 | set.names = append(set.names, "ask_user") | 375 | set.names = append(set.names, "ask_user") |
| 375 | } | 376 | } |
| 377 | + if config.Cfg.SaveAnswerTool { | ||
| 378 | + set.tools = append(set.tools, tools.SaveAnswer(e.G, d)) | ||
| 379 | + set.names = append(set.names, "save_answer") | ||
| 380 | + } | ||
| 376 | if config.Cfg.FetchContentTool { | 381 | if config.Cfg.FetchContentTool { |
| 377 | // The product token "mm" is what the sites' robots.txt are read for. | 382 | // The product token "mm" is what the sites' robots.txt are read for. |
| 378 | fetcher := webmd.NewFetcher("mm/" + version.Short()) | 383 | fetcher := webmd.NewFetcher("mm/" + version.Short()) |