Unload model(s) #34
Merged@k33g wants to merge
feature/unload-model into mainMerged as
07f685d44f12.44 files changed
modified
.tickets/issues/0092-how-to-unload-a-model.yaml +2 -2 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 92 |
| 3 | 3 | title: How to unload a model |
| 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-17T14:24:18.694Z |
| 9 | -updatedAt: 2026-10-18T16:00:00.000Z | |
| 9 | +updatedAt: 2026-10-18T19:30:22.673Z | |
| 10 | 10 | labels: |
| 11 | 11 | - priority::urgent |
| 12 | 12 | milestone: v0.7.0 |
| @@ -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: 92 | 2 | id: 92 |
| 3 | title: How to unload a model | 3 | title: How to unload a model |
| 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-17T14:24:18.694Z | 8 | createdAt: 2026-10-17T14:24:18.694Z |
| 9 | -updatedAt: 2026-10-18T16:00:00.000Z | 9 | +updatedAt: 2026-10-18T19:30:22.673Z |
| 10 | labels: | 10 | labels: |
| 11 | - priority::urgent | 11 | - priority::urgent |
| 12 | milestone: v0.7.0 | 12 | milestone: v0.7.0 |
deleted
.tickets/issues/0101-unload-a-model-command-and-tool.yaml +0 -116 | deleted file mode 100644 | ||
| @@ -1,116 +0,0 @@ | ||
| 1 | -# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | -id: 101 | |
| 3 | -title: Unload a model from memory — /unload command and unload_model tool | |
| 4 | -state: open | |
| 5 | -author: | |
| 6 | - name: k33g | |
| 7 | - email: ph.charriere@gmail.com | |
| 8 | -createdAt: 2026-10-18T16:30:00.000Z | |
| 9 | -updatedAt: 2026-10-18T16:30:00.000Z | |
| 10 | -labels: | |
| 11 | - - kind::feature | |
| 12 | -milestone: v0.7.0 | |
| 13 | -body: | | |
| 14 | - Follows ticket 92 (research: how each server unloads a model). Give mm the | |
| 15 | - capacity to unload a model from the server's memory, for the llmman, ollama | |
| 16 | - and llamacpp providers, in three forms: | |
| 17 | - | |
| 18 | - 1. a slash command `/unload [model]`, in the line mode, the window (-tui) | |
| 19 | - AND over ACP (an editor's slash-command menu); | |
| 20 | - 2. a built-in tool `unload_model`, like `ask_agent`, that the model calls | |
| 21 | - itself when its instructions say so — e.g. "unload your model before you | |
| 22 | - hand the task to another agent", or before handing control back; | |
| 23 | - 3. a demo on llmman in `demos/unload-models/`. | |
| 24 | - | |
| 25 | - The capacity can be turned off in the configuration file. | |
| 26 | - | |
| 27 | - Proposal (to confirm before coding): | |
| 28 | - - Key `unloadModels` (bool, default true). `false`: no `/unload` command | |
| 29 | - (not listed, not advertised over ACP) and no `unload_model` tool. | |
| 30 | - - `/unload` alone unloads THIS agent's model; `/unload <model>` another one | |
| 31 | - served by the same server. The next question reloads the model, as usual. | |
| 32 | - - Tool `unload_model {model?: string}`: empty = this agent's model. The | |
| 33 | - output says what happened, as text (unloaded / was not loaded / not | |
| 34 | - supported by this server, and why), never a Go error. | |
| 35 | - - Per provider (ticket 92 comment 1): | |
| 36 | - - llmman, ollama: POST <root>/api/generate {"model": m, "keep_alive": 0} | |
| 37 | - (root = baseUrl without /v1); success = done_reason "unload". | |
| 38 | - - llamacpp: POST <root>/models/unload {"model": m} — router mode only; a | |
| 39 | - single-model llama-server has no such route: say so, and point to | |
| 40 | - --sleep-idle-seconds. | |
| 41 | - - dmr: out of scope for now — "not supported by this provider". | |
| 42 | - - Permission: `unload_model` changes nothing on disk; it is gated like | |
| 43 | - `ask_agent` (asked when `yolo: false`)? To decide. | |
| 44 | - - Hooks: the tool goes through preToolCall/postToolCall like every tool. | |
| 45 | - | |
| 46 | - Open questions: | |
| 47 | - - Default on or off? (proposal: on, since unloading is harmless — the next | |
| 48 | - request reloads.) | |
| 49 | - - Should the tool be allowed to unload a model other than its own, or only | |
| 50 | - its own (safer for a small model that might unload the wrong one)? | |
| 51 | - - Permission with `yolo: false`: ask, or never ask? | |
| 52 | -tasks: | |
| 53 | - - id: 1 | |
| 54 | - title: "Agree on the key name and default, the tool's scope (own model only or any), and its permission" | |
| 55 | - state: open | |
| 56 | - priority: none | |
| 57 | - author: | |
| 58 | - name: k33g | |
| 59 | - email: ph.charriere@gmail.com | |
| 60 | - createdAt: 2026-10-18T16:30:00.000Z | |
| 61 | - updatedAt: 2026-10-18T16:30:00.000Z | |
| 62 | - - id: 2 | |
| 63 | - title: "engine: Unload(ctx, model) per provider — llmman/ollama /api/generate keep_alive 0, llamacpp /models/unload, others not supported — with tests on a fake server" | |
| 64 | - state: open | |
| 65 | - priority: none | |
| 66 | - author: | |
| 67 | - name: k33g | |
| 68 | - email: ph.charriere@gmail.com | |
| 69 | - createdAt: 2026-10-18T16:30:00.000Z | |
| 70 | - updatedAt: 2026-10-18T16:30:00.000Z | |
| 71 | - - id: 3 | |
| 72 | - title: "config key (unloadModels), validation and banner" | |
| 73 | - state: open | |
| 74 | - priority: none | |
| 75 | - author: | |
| 76 | - name: k33g | |
| 77 | - email: ph.charriere@gmail.com | |
| 78 | - createdAt: 2026-10-18T16:30:00.000Z | |
| 79 | - updatedAt: 2026-10-18T16:30:00.000Z | |
| 80 | - - id: 4 | |
| 81 | - title: "/unload [model] in the command table for Terminal | Window | Editor, handled by the three front ends" | |
| 82 | - state: open | |
| 83 | - priority: none | |
| 84 | - author: | |
| 85 | - name: k33g | |
| 86 | - email: ph.charriere@gmail.com | |
| 87 | - createdAt: 2026-10-18T16:30:00.000Z | |
| 88 | - updatedAt: 2026-10-18T16:30:00.000Z | |
| 89 | - - id: 5 | |
| 90 | - title: "built-in tool unload_model (reserved name, declared unless unloadModels is false)" | |
| 91 | - state: open | |
| 92 | - priority: none | |
| 93 | - author: | |
| 94 | - name: k33g | |
| 95 | - email: ph.charriere@gmail.com | |
| 96 | - createdAt: 2026-10-18T16:30:00.000Z | |
| 97 | - updatedAt: 2026-10-18T16:30:00.000Z | |
| 98 | - - id: 6 | |
| 99 | - title: "demos/unload-models/ on llmman: a coordinator that unloads its model before handing a task to another agent (different model), READMEs EN+FR, measured" | |
| 100 | - state: open | |
| 101 | - priority: none | |
| 102 | - author: | |
| 103 | - name: k33g | |
| 104 | - email: ph.charriere@gmail.com | |
| 105 | - createdAt: 2026-10-18T16:30:00.000Z | |
| 106 | - updatedAt: 2026-10-18T16:30:00.000Z | |
| 107 | - - id: 7 | |
| 108 | - title: "Docs EN+FR (reference: configuration, tools, slash-commands, acp; how-to), quality gate, .memory" | |
| 109 | - state: open | |
| 110 | - priority: none | |
| 111 | - author: | |
| 112 | - name: k33g | |
| 113 | - email: ph.charriere@gmail.com | |
| 114 | - createdAt: 2026-10-18T16:30:00.000Z | |
| 115 | - updatedAt: 2026-10-18T16:30:00.000Z | |
| 116 | -comments: [] | |
| deleted file mode 100644 | |||
| @@ -1,116 +0,0 @@ | |||
| 1 | -# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | -id: 101 | ||
| 3 | -title: Unload a model from memory — /unload command and unload_model tool | ||
| 4 | -state: open | ||
| 5 | -author: | ||
| 6 | - name: k33g | ||
| 7 | - email: ph.charriere@gmail.com | ||
| 8 | -createdAt: 2026-10-18T16:30:00.000Z | ||
| 9 | -updatedAt: 2026-10-18T16:30:00.000Z | ||
| 10 | -labels: | ||
| 11 | - - kind::feature | ||
| 12 | -milestone: v0.7.0 | ||
| 13 | -body: | | ||
| 14 | - Follows ticket 92 (research: how each server unloads a model). Give mm the | ||
| 15 | - capacity to unload a model from the server's memory, for the llmman, ollama | ||
| 16 | - and llamacpp providers, in three forms: | ||
| 17 | - | ||
| 18 | - 1. a slash command `/unload [model]`, in the line mode, the window (-tui) | ||
| 19 | - AND over ACP (an editor's slash-command menu); | ||
| 20 | - 2. a built-in tool `unload_model`, like `ask_agent`, that the model calls | ||
| 21 | - itself when its instructions say so — e.g. "unload your model before you | ||
| 22 | - hand the task to another agent", or before handing control back; | ||
| 23 | - 3. a demo on llmman in `demos/unload-models/`. | ||
| 24 | - | ||
| 25 | - The capacity can be turned off in the configuration file. | ||
| 26 | - | ||
| 27 | - Proposal (to confirm before coding): | ||
| 28 | - - Key `unloadModels` (bool, default true). `false`: no `/unload` command | ||
| 29 | - (not listed, not advertised over ACP) and no `unload_model` tool. | ||
| 30 | - - `/unload` alone unloads THIS agent's model; `/unload <model>` another one | ||
| 31 | - served by the same server. The next question reloads the model, as usual. | ||
| 32 | - - Tool `unload_model {model?: string}`: empty = this agent's model. The | ||
| 33 | - output says what happened, as text (unloaded / was not loaded / not | ||
| 34 | - supported by this server, and why), never a Go error. | ||
| 35 | - - Per provider (ticket 92 comment 1): | ||
| 36 | - - llmman, ollama: POST <root>/api/generate {"model": m, "keep_alive": 0} | ||
| 37 | - (root = baseUrl without /v1); success = done_reason "unload". | ||
| 38 | - - llamacpp: POST <root>/models/unload {"model": m} — router mode only; a | ||
| 39 | - single-model llama-server has no such route: say so, and point to | ||
| 40 | - --sleep-idle-seconds. | ||
| 41 | - - dmr: out of scope for now — "not supported by this provider". | ||
| 42 | - - Permission: `unload_model` changes nothing on disk; it is gated like | ||
| 43 | - `ask_agent` (asked when `yolo: false`)? To decide. | ||
| 44 | - - Hooks: the tool goes through preToolCall/postToolCall like every tool. | ||
| 45 | - | ||
| 46 | - Open questions: | ||
| 47 | - - Default on or off? (proposal: on, since unloading is harmless — the next | ||
| 48 | - request reloads.) | ||
| 49 | - - Should the tool be allowed to unload a model other than its own, or only | ||
| 50 | - its own (safer for a small model that might unload the wrong one)? | ||
| 51 | - - Permission with `yolo: false`: ask, or never ask? | ||
| 52 | -tasks: | ||
| 53 | - - id: 1 | ||
| 54 | - title: "Agree on the key name and default, the tool's scope (own model only or any), and its permission" | ||
| 55 | - state: open | ||
| 56 | - priority: none | ||
| 57 | - author: | ||
| 58 | - name: k33g | ||
| 59 | - email: ph.charriere@gmail.com | ||
| 60 | - createdAt: 2026-10-18T16:30:00.000Z | ||
| 61 | - updatedAt: 2026-10-18T16:30:00.000Z | ||
| 62 | - - id: 2 | ||
| 63 | - title: "engine: Unload(ctx, model) per provider — llmman/ollama /api/generate keep_alive 0, llamacpp /models/unload, others not supported — with tests on a fake server" | ||
| 64 | - state: open | ||
| 65 | - priority: none | ||
| 66 | - author: | ||
| 67 | - name: k33g | ||
| 68 | - email: ph.charriere@gmail.com | ||
| 69 | - createdAt: 2026-10-18T16:30:00.000Z | ||
| 70 | - updatedAt: 2026-10-18T16:30:00.000Z | ||
| 71 | - - id: 3 | ||
| 72 | - title: "config key (unloadModels), validation and banner" | ||
| 73 | - state: open | ||
| 74 | - priority: none | ||
| 75 | - author: | ||
| 76 | - name: k33g | ||
| 77 | - email: ph.charriere@gmail.com | ||
| 78 | - createdAt: 2026-10-18T16:30:00.000Z | ||
| 79 | - updatedAt: 2026-10-18T16:30:00.000Z | ||
| 80 | - - id: 4 | ||
| 81 | - title: "/unload [model] in the command table for Terminal | Window | Editor, handled by the three front ends" | ||
| 82 | - state: open | ||
| 83 | - priority: none | ||
| 84 | - author: | ||
| 85 | - name: k33g | ||
| 86 | - email: ph.charriere@gmail.com | ||
| 87 | - createdAt: 2026-10-18T16:30:00.000Z | ||
| 88 | - updatedAt: 2026-10-18T16:30:00.000Z | ||
| 89 | - - id: 5 | ||
| 90 | - title: "built-in tool unload_model (reserved name, declared unless unloadModels is false)" | ||
| 91 | - state: open | ||
| 92 | - priority: none | ||
| 93 | - author: | ||
| 94 | - name: k33g | ||
| 95 | - email: ph.charriere@gmail.com | ||
| 96 | - createdAt: 2026-10-18T16:30:00.000Z | ||
| 97 | - updatedAt: 2026-10-18T16:30:00.000Z | ||
| 98 | - - id: 6 | ||
| 99 | - title: "demos/unload-models/ on llmman: a coordinator that unloads its model before handing a task to another agent (different model), READMEs EN+FR, measured" | ||
| 100 | - state: open | ||
| 101 | - priority: none | ||
| 102 | - author: | ||
| 103 | - name: k33g | ||
| 104 | - email: ph.charriere@gmail.com | ||
| 105 | - createdAt: 2026-10-18T16:30:00.000Z | ||
| 106 | - updatedAt: 2026-10-18T16:30:00.000Z | ||
| 107 | - - id: 7 | ||
| 108 | - title: "Docs EN+FR (reference: configuration, tools, slash-commands, acp; how-to), quality gate, .memory" | ||
| 109 | - state: open | ||
| 110 | - priority: none | ||
| 111 | - author: | ||
| 112 | - name: k33g | ||
| 113 | - email: ph.charriere@gmail.com | ||
| 114 | - createdAt: 2026-10-18T16:30:00.000Z | ||
| 115 | - updatedAt: 2026-10-18T16:30:00.000Z | ||
| 116 | -comments: [] | ||
added
.tickets/issues/0101-unload-a-model-from-memory-unload-command-and-un.yaml +197 -0 | new file mode 100644 | ||
| @@ -0,0 +1,197 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 101 | |
| 3 | +title: Unload a model from memory — /unload command and unload_model tool | |
| 4 | +state: closed | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-10-18T16:30:00.000Z | |
| 9 | +updatedAt: 2026-10-18T19:30:11.700Z | |
| 10 | +labels: | |
| 11 | + - kind::feature | |
| 12 | +milestone: v0.7.0 | |
| 13 | +related: | |
| 14 | + - 92 | |
| 15 | +body: | | |
| 16 | + Follows ticket 92 (research: how each server unloads a model). Give mm the | |
| 17 | + capacity to unload a model from the server's memory, for the llmman, ollama | |
| 18 | + and llamacpp providers, in three forms: | |
| 19 | + | |
| 20 | + 1. a slash command `/unload [model]`, in the line mode, the window (-tui) | |
| 21 | + AND over ACP (an editor's slash-command menu); | |
| 22 | + 2. a built-in tool `unload_model`, like `ask_agent`, that the model calls | |
| 23 | + itself when its instructions say so — e.g. "unload your model before you | |
| 24 | + hand the task to another agent", or before handing control back; | |
| 25 | + 3. a demo on llmman in `demos/unload-models/`. | |
| 26 | + | |
| 27 | + The capacity can be turned off in the configuration file. | |
| 28 | + | |
| 29 | + Proposal (to confirm before coding): | |
| 30 | + - Key `unloadModels` (bool, default true). `false`: no `/unload` command | |
| 31 | + (not listed, not advertised over ACP) and no `unload_model` tool. | |
| 32 | + - `/unload` alone unloads THIS agent's model; `/unload <model>` another one | |
| 33 | + served by the same server. The next question reloads the model, as usual. | |
| 34 | + - Tool `unload_model {model?: string}`: empty = this agent's model. The | |
| 35 | + output says what happened, as text (unloaded / was not loaded / not | |
| 36 | + supported by this server, and why), never a Go error. | |
| 37 | + - Per provider (ticket 92 comment 1): | |
| 38 | + - llmman, ollama: POST <root>/api/generate {"model": m, "keep_alive": 0} | |
| 39 | + (root = baseUrl without /v1); success = done_reason "unload". | |
| 40 | + - llamacpp: POST <root>/models/unload {"model": m} — router mode only; a | |
| 41 | + single-model llama-server has no such route: say so, and point to | |
| 42 | + --sleep-idle-seconds. | |
| 43 | + - dmr: out of scope for now — "not supported by this provider". | |
| 44 | + - Permission: `unload_model` changes nothing on disk; it is gated like | |
| 45 | + `ask_agent` (asked when `yolo: false`)? To decide. | |
| 46 | + - Hooks: the tool goes through preToolCall/postToolCall like every tool. | |
| 47 | + | |
| 48 | + Open questions: | |
| 49 | + - Default on or off? (proposal: on, since unloading is harmless — the next | |
| 50 | + request reloads.) | |
| 51 | + - Should the tool be allowed to unload a model other than its own, or only | |
| 52 | + its own (safer for a small model that might unload the wrong one)? | |
| 53 | + - Permission with `yolo: false`: ask, or never ask? | |
| 54 | +tasks: | |
| 55 | + - id: 1 | |
| 56 | + title: Agree on the key name and default, the tool's scope (own model only or any), and its permission | |
| 57 | + state: closed | |
| 58 | + priority: none | |
| 59 | + author: | |
| 60 | + name: k33g | |
| 61 | + email: ph.charriere@gmail.com | |
| 62 | + createdAt: 2026-10-18T16:30:00.000Z | |
| 63 | + updatedAt: 2026-10-18T16:30:00.000Z | |
| 64 | + - id: 2 | |
| 65 | + title: "engine: Unload(ctx, model) per provider — llmman/ollama /api/generate keep_alive 0, llamacpp /models/unload, others not supported — with tests on a fake server" | |
| 66 | + state: closed | |
| 67 | + priority: none | |
| 68 | + author: | |
| 69 | + name: k33g | |
| 70 | + email: ph.charriere@gmail.com | |
| 71 | + createdAt: 2026-10-18T16:30:00.000Z | |
| 72 | + updatedAt: 2026-10-18T16:30:00.000Z | |
| 73 | + - id: 3 | |
| 74 | + title: config key (unloadModels), validation and banner | |
| 75 | + state: closed | |
| 76 | + priority: none | |
| 77 | + author: | |
| 78 | + name: k33g | |
| 79 | + email: ph.charriere@gmail.com | |
| 80 | + createdAt: 2026-10-18T16:30:00.000Z | |
| 81 | + updatedAt: 2026-10-18T16:30:00.000Z | |
| 82 | + - id: 4 | |
| 83 | + title: /unload [model] in the command table for Terminal | Window | Editor, handled by the three front ends | |
| 84 | + state: closed | |
| 85 | + priority: none | |
| 86 | + author: | |
| 87 | + name: k33g | |
| 88 | + email: ph.charriere@gmail.com | |
| 89 | + createdAt: 2026-10-18T16:30:00.000Z | |
| 90 | + updatedAt: 2026-10-18T16:30:00.000Z | |
| 91 | + - id: 5 | |
| 92 | + title: built-in tool unload_model (reserved name, declared unless unloadModels is false) | |
| 93 | + state: closed | |
| 94 | + priority: none | |
| 95 | + author: | |
| 96 | + name: k33g | |
| 97 | + email: ph.charriere@gmail.com | |
| 98 | + createdAt: 2026-10-18T16:30:00.000Z | |
| 99 | + updatedAt: 2026-10-18T16:30:00.000Z | |
| 100 | + - id: 6 | |
| 101 | + title: "demos/unload-models/ on llmman: a coordinator that unloads its model before handing a task to another agent (different model), READMEs EN+FR, measured" | |
| 102 | + state: closed | |
| 103 | + priority: none | |
| 104 | + author: | |
| 105 | + name: k33g | |
| 106 | + email: ph.charriere@gmail.com | |
| 107 | + createdAt: 2026-10-18T16:30:00.000Z | |
| 108 | + updatedAt: 2026-10-18T16:30:00.000Z | |
| 109 | + - id: 7 | |
| 110 | + title: "Docs EN+FR (reference: configuration, tools, slash-commands, acp; how-to), quality gate, .memory" | |
| 111 | + state: open | |
| 112 | + priority: none | |
| 113 | + author: | |
| 114 | + name: k33g | |
| 115 | + email: ph.charriere@gmail.com | |
| 116 | + createdAt: 2026-10-18T16:30:00.000Z | |
| 117 | + updatedAt: 2026-10-18T16:30:00.000Z | |
| 118 | +comments: | |
| 119 | + - id: 1 | |
| 120 | + author: | |
| 121 | + name: k33g | |
| 122 | + email: ph.charriere@gmail.com | |
| 123 | + createdAt: 2026-10-18T16:45:00.000Z | |
| 124 | + body: | | |
| 125 | + Decided by the user: `unloadModels` ON by default; the tool may unload ANY | |
| 126 | + model (`model` empty = this agent's); never asks for permission, even with | |
| 127 | + `yolo: false` (like read_file: nothing changes on disk, the next request | |
| 128 | + reloads). Branch created by the user. | |
| 129 | + - id: 2 | |
| 130 | + author: | |
| 131 | + name: k33g | |
| 132 | + email: ph.charriere@gmail.com | |
| 133 | + createdAt: 2026-10-18T18:00:00.000Z | |
| 134 | + body: | | |
| 135 | + Done on feature/unload-model (tasks 2-6; task 7 open: quality gate not run, | |
| 136 | + qlty absent from this sandbox — gofmt/go vet clean, go test ./... green). | |
| 137 | + - engine/unload.go: Engine.Unload(ctx, model) → one line; per provider: | |
| 138 | + llmman/ollama = /api/ps lookup first (a keep_alive 0 generate on a model | |
| 139 | + that is NOT loaded would load it), then POST /api/generate keep_alive 0, | |
| 140 | + done_reason "unload"; llamacpp = POST /models/unload (router mode; a 404 | |
| 141 | + explains single-model mode and --sleep-idle-seconds); dmr = not supported. | |
| 142 | + - config unloadModels (default true, user's choice, confirmed after being | |
| 143 | + told every existing agent gets one more tool); reserved name unload_model. | |
| 144 | + - /unload [model]: command.SetUnload, Terminal|Window|Editor; REPL, window | |
| 145 | + (off the event loop) and ACP handlers. Tool unload_model (any model, empty | |
| 146 | + = own), never gated, not beside a structuredOutput. | |
| 147 | + - Finding: unloading the agent's OWN model through the tool lasts one step | |
| 148 | + (it needs its model to read the result) — said in the tool description; | |
| 149 | + the demo unloads the asked agent's model instead. | |
| 150 | + - Live: /unload over ACP on llmman (unloaded → "nothing to unload" → | |
| 151 | + unknown model); demos/guard, demos/guards and structured-output-pizza | |
| 152 | + still work with the default. demos/unload-models: MiniCPM5 2B coordinator | |
| 153 | + + Qwen3.5 9B expert, unload_model called 4/5 (after a prompt fix; first | |
| 154 | + prompt skipped it), expert gone from /api/ps each time it was called. | |
| 155 | + - Docs EN+FR: reference configuration / slash-commands / tools, how-to | |
| 156 | + unload-a-model, indexes, README, demo READMEs. | |
| 157 | + - id: 3 | |
| 158 | + author: | |
| 159 | + name: k33g | |
| 160 | + email: ph.charriere@gmail.com | |
| 161 | + createdAt: 2026-10-18T19:30:00.000Z | |
| 162 | + body: | | |
| 163 | + Follow-ups (user's requests): | |
| 164 | + - "unload the main model before the other agent starts": the model cannot | |
| 165 | + (it is needed until the ask_agent call and again to read the answer). | |
| 166 | + User chose a preAsk hook in the demo over an mmConfigs option: | |
| 167 | + demos/unload-models/hooks/unload-self.js (keep_alive 0 on the asking | |
| 168 | + agent's model, read from the hook input "model"). Measured with /api/ps | |
| 169 | + every 0.5 s on 3 questions: coordinator alone → nothing → expert ALONE → | |
| 170 | + both (coordinator reloads to read) → coordinator; hook 3/3, tool 3/3. | |
| 171 | + - "/unload not visible in -tui": the "/" picker drew only the first 8 | |
| 172 | + choices (pickerRows) and never scrolled; /unload is the 10th command. | |
| 173 | + Pre-existing bug (it also hid /ask-<name> commands). Fixed: the window | |
| 174 | + follows the selection (tui/draw.go pickerFirst, + test). | |
| 175 | + - "unload_model calls not visible over ACP": over the real protocol the | |
| 176 | + editor receives the tool_call ("unload_model: <model>", pending → | |
| 177 | + in_progress → completed) — checked with a scripted client on the demo. | |
| 178 | + Most likely the editor runs an older installed mm; not reproduced here. | |
| 179 | + - id: 4 | |
| 180 | + author: | |
| 181 | + name: k33g | |
| 182 | + email: ph.charriere@gmail.com | |
| 183 | + createdAt: 2026-10-18T21:30:00.000Z | |
| 184 | + body: | | |
| 185 | + Demo follow-ups (user's requests): | |
| 186 | + - Models: coordinator Qwen3.5 9B, expert Qwen3.5 4B (was Ornith 1.5 9B / | |
| 187 | + Qwen3.5 9B). With preAsk + unload_model: 7/7 clean, but both models | |
| 188 | + loaded together ~18 s per question (the coordinator reloads to read the | |
| 189 | + answer before it can call unload_model). | |
| 190 | + - "never two models loaded at once": hooks/unload-self.js became | |
| 191 | + hooks/unload.js self|asked; a postAsk hook (unload.js asked, model read | |
| 192 | + from ask.agentModel) unloads the expert before the coordinator reloads. | |
| 193 | + unload_model step removed from the prompt (kept for "free the expert's | |
| 194 | + memory" requests; expert model name in the prompt). Measured 9/9: | |
| 195 | + 9B → nothing → 4B → nothing → 9B; "unload the expert" 4/4. Prompt | |
| 196 | + needed "ask_agent is ALWAYS your first action" (short prompt let the | |
| 197 | + coordinator answer yield itself, 1/7). | |
| new file mode 100644 | |||
| @@ -0,0 +1,197 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 101 | ||
| 3 | +title: Unload a model from memory — /unload command and unload_model tool | ||
| 4 | +state: closed | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-10-18T16:30:00.000Z | ||
| 9 | +updatedAt: 2026-10-18T19:30:11.700Z | ||
| 10 | +labels: | ||
| 11 | + - kind::feature | ||
| 12 | +milestone: v0.7.0 | ||
| 13 | +related: | ||
| 14 | + - 92 | ||
| 15 | +body: | | ||
| 16 | + Follows ticket 92 (research: how each server unloads a model). Give mm the | ||
| 17 | + capacity to unload a model from the server's memory, for the llmman, ollama | ||
| 18 | + and llamacpp providers, in three forms: | ||
| 19 | + | ||
| 20 | + 1. a slash command `/unload [model]`, in the line mode, the window (-tui) | ||
| 21 | + AND over ACP (an editor's slash-command menu); | ||
| 22 | + 2. a built-in tool `unload_model`, like `ask_agent`, that the model calls | ||
| 23 | + itself when its instructions say so — e.g. "unload your model before you | ||
| 24 | + hand the task to another agent", or before handing control back; | ||
| 25 | + 3. a demo on llmman in `demos/unload-models/`. | ||
| 26 | + | ||
| 27 | + The capacity can be turned off in the configuration file. | ||
| 28 | + | ||
| 29 | + Proposal (to confirm before coding): | ||
| 30 | + - Key `unloadModels` (bool, default true). `false`: no `/unload` command | ||
| 31 | + (not listed, not advertised over ACP) and no `unload_model` tool. | ||
| 32 | + - `/unload` alone unloads THIS agent's model; `/unload <model>` another one | ||
| 33 | + served by the same server. The next question reloads the model, as usual. | ||
| 34 | + - Tool `unload_model {model?: string}`: empty = this agent's model. The | ||
| 35 | + output says what happened, as text (unloaded / was not loaded / not | ||
| 36 | + supported by this server, and why), never a Go error. | ||
| 37 | + - Per provider (ticket 92 comment 1): | ||
| 38 | + - llmman, ollama: POST <root>/api/generate {"model": m, "keep_alive": 0} | ||
| 39 | + (root = baseUrl without /v1); success = done_reason "unload". | ||
| 40 | + - llamacpp: POST <root>/models/unload {"model": m} — router mode only; a | ||
| 41 | + single-model llama-server has no such route: say so, and point to | ||
| 42 | + --sleep-idle-seconds. | ||
| 43 | + - dmr: out of scope for now — "not supported by this provider". | ||
| 44 | + - Permission: `unload_model` changes nothing on disk; it is gated like | ||
| 45 | + `ask_agent` (asked when `yolo: false`)? To decide. | ||
| 46 | + - Hooks: the tool goes through preToolCall/postToolCall like every tool. | ||
| 47 | + | ||
| 48 | + Open questions: | ||
| 49 | + - Default on or off? (proposal: on, since unloading is harmless — the next | ||
| 50 | + request reloads.) | ||
| 51 | + - Should the tool be allowed to unload a model other than its own, or only | ||
| 52 | + its own (safer for a small model that might unload the wrong one)? | ||
| 53 | + - Permission with `yolo: false`: ask, or never ask? | ||
| 54 | +tasks: | ||
| 55 | + - id: 1 | ||
| 56 | + title: Agree on the key name and default, the tool's scope (own model only or any), and its permission | ||
| 57 | + state: closed | ||
| 58 | + priority: none | ||
| 59 | + author: | ||
| 60 | + name: k33g | ||
| 61 | + email: ph.charriere@gmail.com | ||
| 62 | + createdAt: 2026-10-18T16:30:00.000Z | ||
| 63 | + updatedAt: 2026-10-18T16:30:00.000Z | ||
| 64 | + - id: 2 | ||
| 65 | + title: "engine: Unload(ctx, model) per provider — llmman/ollama /api/generate keep_alive 0, llamacpp /models/unload, others not supported — with tests on a fake server" | ||
| 66 | + state: closed | ||
| 67 | + priority: none | ||
| 68 | + author: | ||
| 69 | + name: k33g | ||
| 70 | + email: ph.charriere@gmail.com | ||
| 71 | + createdAt: 2026-10-18T16:30:00.000Z | ||
| 72 | + updatedAt: 2026-10-18T16:30:00.000Z | ||
| 73 | + - id: 3 | ||
| 74 | + title: config key (unloadModels), validation and banner | ||
| 75 | + state: closed | ||
| 76 | + priority: none | ||
| 77 | + author: | ||
| 78 | + name: k33g | ||
| 79 | + email: ph.charriere@gmail.com | ||
| 80 | + createdAt: 2026-10-18T16:30:00.000Z | ||
| 81 | + updatedAt: 2026-10-18T16:30:00.000Z | ||
| 82 | + - id: 4 | ||
| 83 | + title: /unload [model] in the command table for Terminal | Window | Editor, handled by the three front ends | ||
| 84 | + state: closed | ||
| 85 | + priority: none | ||
| 86 | + author: | ||
| 87 | + name: k33g | ||
| 88 | + email: ph.charriere@gmail.com | ||
| 89 | + createdAt: 2026-10-18T16:30:00.000Z | ||
| 90 | + updatedAt: 2026-10-18T16:30:00.000Z | ||
| 91 | + - id: 5 | ||
| 92 | + title: built-in tool unload_model (reserved name, declared unless unloadModels is false) | ||
| 93 | + state: closed | ||
| 94 | + priority: none | ||
| 95 | + author: | ||
| 96 | + name: k33g | ||
| 97 | + email: ph.charriere@gmail.com | ||
| 98 | + createdAt: 2026-10-18T16:30:00.000Z | ||
| 99 | + updatedAt: 2026-10-18T16:30:00.000Z | ||
| 100 | + - id: 6 | ||
| 101 | + title: "demos/unload-models/ on llmman: a coordinator that unloads its model before handing a task to another agent (different model), READMEs EN+FR, measured" | ||
| 102 | + state: closed | ||
| 103 | + priority: none | ||
| 104 | + author: | ||
| 105 | + name: k33g | ||
| 106 | + email: ph.charriere@gmail.com | ||
| 107 | + createdAt: 2026-10-18T16:30:00.000Z | ||
| 108 | + updatedAt: 2026-10-18T16:30:00.000Z | ||
| 109 | + - id: 7 | ||
| 110 | + title: "Docs EN+FR (reference: configuration, tools, slash-commands, acp; how-to), quality gate, .memory" | ||
| 111 | + state: open | ||
| 112 | + priority: none | ||
| 113 | + author: | ||
| 114 | + name: k33g | ||
| 115 | + email: ph.charriere@gmail.com | ||
| 116 | + createdAt: 2026-10-18T16:30:00.000Z | ||
| 117 | + updatedAt: 2026-10-18T16:30:00.000Z | ||
| 118 | +comments: | ||
| 119 | + - id: 1 | ||
| 120 | + author: | ||
| 121 | + name: k33g | ||
| 122 | + email: ph.charriere@gmail.com | ||
| 123 | + createdAt: 2026-10-18T16:45:00.000Z | ||
| 124 | + body: | | ||
| 125 | + Decided by the user: `unloadModels` ON by default; the tool may unload ANY | ||
| 126 | + model (`model` empty = this agent's); never asks for permission, even with | ||
| 127 | + `yolo: false` (like read_file: nothing changes on disk, the next request | ||
| 128 | + reloads). Branch created by the user. | ||
| 129 | + - id: 2 | ||
| 130 | + author: | ||
| 131 | + name: k33g | ||
| 132 | + email: ph.charriere@gmail.com | ||
| 133 | + createdAt: 2026-10-18T18:00:00.000Z | ||
| 134 | + body: | | ||
| 135 | + Done on feature/unload-model (tasks 2-6; task 7 open: quality gate not run, | ||
| 136 | + qlty absent from this sandbox — gofmt/go vet clean, go test ./... green). | ||
| 137 | + - engine/unload.go: Engine.Unload(ctx, model) → one line; per provider: | ||
| 138 | + llmman/ollama = /api/ps lookup first (a keep_alive 0 generate on a model | ||
| 139 | + that is NOT loaded would load it), then POST /api/generate keep_alive 0, | ||
| 140 | + done_reason "unload"; llamacpp = POST /models/unload (router mode; a 404 | ||
| 141 | + explains single-model mode and --sleep-idle-seconds); dmr = not supported. | ||
| 142 | + - config unloadModels (default true, user's choice, confirmed after being | ||
| 143 | + told every existing agent gets one more tool); reserved name unload_model. | ||
| 144 | + - /unload [model]: command.SetUnload, Terminal|Window|Editor; REPL, window | ||
| 145 | + (off the event loop) and ACP handlers. Tool unload_model (any model, empty | ||
| 146 | + = own), never gated, not beside a structuredOutput. | ||
| 147 | + - Finding: unloading the agent's OWN model through the tool lasts one step | ||
| 148 | + (it needs its model to read the result) — said in the tool description; | ||
| 149 | + the demo unloads the asked agent's model instead. | ||
| 150 | + - Live: /unload over ACP on llmman (unloaded → "nothing to unload" → | ||
| 151 | + unknown model); demos/guard, demos/guards and structured-output-pizza | ||
| 152 | + still work with the default. demos/unload-models: MiniCPM5 2B coordinator | ||
| 153 | + + Qwen3.5 9B expert, unload_model called 4/5 (after a prompt fix; first | ||
| 154 | + prompt skipped it), expert gone from /api/ps each time it was called. | ||
| 155 | + - Docs EN+FR: reference configuration / slash-commands / tools, how-to | ||
| 156 | + unload-a-model, indexes, README, demo READMEs. | ||
| 157 | + - id: 3 | ||
| 158 | + author: | ||
| 159 | + name: k33g | ||
| 160 | + email: ph.charriere@gmail.com | ||
| 161 | + createdAt: 2026-10-18T19:30:00.000Z | ||
| 162 | + body: | | ||
| 163 | + Follow-ups (user's requests): | ||
| 164 | + - "unload the main model before the other agent starts": the model cannot | ||
| 165 | + (it is needed until the ask_agent call and again to read the answer). | ||
| 166 | + User chose a preAsk hook in the demo over an mmConfigs option: | ||
| 167 | + demos/unload-models/hooks/unload-self.js (keep_alive 0 on the asking | ||
| 168 | + agent's model, read from the hook input "model"). Measured with /api/ps | ||
| 169 | + every 0.5 s on 3 questions: coordinator alone → nothing → expert ALONE → | ||
| 170 | + both (coordinator reloads to read) → coordinator; hook 3/3, tool 3/3. | ||
| 171 | + - "/unload not visible in -tui": the "/" picker drew only the first 8 | ||
| 172 | + choices (pickerRows) and never scrolled; /unload is the 10th command. | ||
| 173 | + Pre-existing bug (it also hid /ask-<name> commands). Fixed: the window | ||
| 174 | + follows the selection (tui/draw.go pickerFirst, + test). | ||
| 175 | + - "unload_model calls not visible over ACP": over the real protocol the | ||
| 176 | + editor receives the tool_call ("unload_model: <model>", pending → | ||
| 177 | + in_progress → completed) — checked with a scripted client on the demo. | ||
| 178 | + Most likely the editor runs an older installed mm; not reproduced here. | ||
| 179 | + - id: 4 | ||
| 180 | + author: | ||
| 181 | + name: k33g | ||
| 182 | + email: ph.charriere@gmail.com | ||
| 183 | + createdAt: 2026-10-18T21:30:00.000Z | ||
| 184 | + body: | | ||
| 185 | + Demo follow-ups (user's requests): | ||
| 186 | + - Models: coordinator Qwen3.5 9B, expert Qwen3.5 4B (was Ornith 1.5 9B / | ||
| 187 | + Qwen3.5 9B). With preAsk + unload_model: 7/7 clean, but both models | ||
| 188 | + loaded together ~18 s per question (the coordinator reloads to read the | ||
| 189 | + answer before it can call unload_model). | ||
| 190 | + - "never two models loaded at once": hooks/unload-self.js became | ||
| 191 | + hooks/unload.js self|asked; a postAsk hook (unload.js asked, model read | ||
| 192 | + from ask.agentModel) unloads the expert before the coordinator reloads. | ||
| 193 | + unload_model step removed from the prompt (kept for "free the expert's | ||
| 194 | + memory" requests; expert model name in the prompt). Measured 9/9: | ||
| 195 | + 9B → nothing → 4B → nothing → 9B; "unload the expert" 4/4. Prompt | ||
| 196 | + needed "ask_agent is ALWAYS your first action" (short prompt let the | ||
| 197 | + coordinator answer yield itself, 1/7). | ||
modified
README.md +2 -0 | @@ -120,6 +120,7 @@ Anything that speaks ACP can host `mm -acp` the same way: **JetBrains IDEs** (In | ||
| 120 | 120 | - **Guard models**: have every question checked by guard models (IBM's Granite Guardian), one agent per risk, before your assistant answers — a `preGeneration` hook asks them without the conversation history, and the assistant refuses or answers according to their verdicts. See [Screen questions with guard models](docs/en/how-to/screen-questions-with-guard-models.md), `demos/guard/` and `demos/guards/`. |
| 121 | 121 | - **Plans**: the built-in `plan` tool lets the agent turn a list of tasks into a tracked plan and show its progress after each answer. See [Manage plans and tasks](docs/en/how-to/manage-plans-and-tasks.md). |
| 122 | 122 | - **Save an answer**: `/save` writes the last answer to a Markdown file; with `saveAnswerTool: true`, the model does it itself when you ask in your own words (`save_answer`). Never overwrites a file. See [Save an answer to a file](docs/en/how-to/save-an-answer.md). |
| 123 | +- **Unload a model**: `/unload [model]` frees a model from the server's memory — in the terminal, the window and an editor —, and the `unload_model` tool lets the agent do it itself, e.g. on another agent's model once it has answered; llmman, Ollama and llama-server in router mode. `unloadModels: false` turns both off. See [Free a model's memory](docs/en/how-to/unload-a-model.md) and `demos/unload-models/`. | |
| 123 | 124 | - **Docker Sandboxes**: the sandbox kit lives in its own repository, [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit); `sbx/` explains how to use it (`sbx run …`), with two mixins (`go-toolchain`, `cli-tools`) and ready-made configurations — see `sbx/README.md` and [Run mini-me in a Docker Sandbox](docs/en/how-to/run-in-a-docker-sandbox.md). |
| 124 | 125 | |
| 125 | 126 | ## Demos |
| @@ -152,6 +153,7 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h | ||
| 152 | 153 | | `demos/programming-answers-2/` | **Tuto**: an agent that answers with mini tutorials in the conversation and saves one to Markdown only when asked (`fetch_content` + `save_answer`), Mellum2.1 12B Thinking on llmman, measured results | |
| 153 | 154 | | `demos/guard/` | one guard model (Granite Guardian 3.2 3B-A800M on llmman) answering `Yes`/`No` to "is this question harmful?"; why its `system:` line is a risk name, not a prompt; custom risks | |
| 154 | 155 | | `demos/guards/` | an assistant (Gemma 4 E4B) protected by seven guard agents, one per risk: a Node.js `preGeneration` hook asks them in parallel without the history, the assistant refuses or answers according to their verdicts; `displayHooks: true`; measured results | |
| 156 | +| `demos/unload-models/` | a coordinator (Qwen3.5 9B) hands programming questions to an expert (Qwen3.5 4B) with `ask_agent`, a `preAsk` hook unloads the coordinator's own model before the expert starts and a `postAsk` hook unloads the expert's once it has answered — the two models are never loaded together; `/unload` and `unload_model` on request; llmman, measured results | | |
| 155 | 157 | | `demos/declared-tools/` | an ops desk with no `bash` and no skills, acting only through four tools declared under `tools:` — release, version bump, service logs, slow requests — covering every parameter type (string, enum, boolean, integer, number), a safe default (`dryRun: true`) and an optional argument; Mellum2 12B on DMR, measured results | |
| 156 | 158 | |
| 157 | 159 | ## Learn more |
| @@ -120,6 +120,7 @@ Anything that speaks ACP can host `mm -acp` the same way: **JetBrains IDEs** (In | |||
| 120 | - **Guard models**: have every question checked by guard models (IBM's Granite Guardian), one agent per risk, before your assistant answers — a `preGeneration` hook asks them without the conversation history, and the assistant refuses or answers according to their verdicts. See [Screen questions with guard models](docs/en/how-to/screen-questions-with-guard-models.md), `demos/guard/` and `demos/guards/`. | 120 | - **Guard models**: have every question checked by guard models (IBM's Granite Guardian), one agent per risk, before your assistant answers — a `preGeneration` hook asks them without the conversation history, and the assistant refuses or answers according to their verdicts. See [Screen questions with guard models](docs/en/how-to/screen-questions-with-guard-models.md), `demos/guard/` and `demos/guards/`. |
| 121 | - **Plans**: the built-in `plan` tool lets the agent turn a list of tasks into a tracked plan and show its progress after each answer. See [Manage plans and tasks](docs/en/how-to/manage-plans-and-tasks.md). | 121 | - **Plans**: the built-in `plan` tool lets the agent turn a list of tasks into a tracked plan and show its progress after each answer. See [Manage plans and tasks](docs/en/how-to/manage-plans-and-tasks.md). |
| 122 | - **Save an answer**: `/save` writes the last answer to a Markdown file; with `saveAnswerTool: true`, the model does it itself when you ask in your own words (`save_answer`). Never overwrites a file. See [Save an answer to a file](docs/en/how-to/save-an-answer.md). | 122 | - **Save an answer**: `/save` writes the last answer to a Markdown file; with `saveAnswerTool: true`, the model does it itself when you ask in your own words (`save_answer`). Never overwrites a file. See [Save an answer to a file](docs/en/how-to/save-an-answer.md). |
| 123 | +- **Unload a model**: `/unload [model]` frees a model from the server's memory — in the terminal, the window and an editor —, and the `unload_model` tool lets the agent do it itself, e.g. on another agent's model once it has answered; llmman, Ollama and llama-server in router mode. `unloadModels: false` turns both off. See [Free a model's memory](docs/en/how-to/unload-a-model.md) and `demos/unload-models/`. | ||
| 123 | - **Docker Sandboxes**: the sandbox kit lives in its own repository, [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit); `sbx/` explains how to use it (`sbx run …`), with two mixins (`go-toolchain`, `cli-tools`) and ready-made configurations — see `sbx/README.md` and [Run mini-me in a Docker Sandbox](docs/en/how-to/run-in-a-docker-sandbox.md). | 124 | - **Docker Sandboxes**: the sandbox kit lives in its own repository, [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit); `sbx/` explains how to use it (`sbx run …`), with two mixins (`go-toolchain`, `cli-tools`) and ready-made configurations — see `sbx/README.md` and [Run mini-me in a Docker Sandbox](docs/en/how-to/run-in-a-docker-sandbox.md). |
| 124 | 125 | ||
| 125 | ## Demos | 126 | ## Demos |
| @@ -152,6 +153,7 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h | |||
| 152 | | `demos/programming-answers-2/` | **Tuto**: an agent that answers with mini tutorials in the conversation and saves one to Markdown only when asked (`fetch_content` + `save_answer`), Mellum2.1 12B Thinking on llmman, measured results | | 153 | | `demos/programming-answers-2/` | **Tuto**: an agent that answers with mini tutorials in the conversation and saves one to Markdown only when asked (`fetch_content` + `save_answer`), Mellum2.1 12B Thinking on llmman, measured results | |
| 153 | | `demos/guard/` | one guard model (Granite Guardian 3.2 3B-A800M on llmman) answering `Yes`/`No` to "is this question harmful?"; why its `system:` line is a risk name, not a prompt; custom risks | | 154 | | `demos/guard/` | one guard model (Granite Guardian 3.2 3B-A800M on llmman) answering `Yes`/`No` to "is this question harmful?"; why its `system:` line is a risk name, not a prompt; custom risks | |
| 154 | | `demos/guards/` | an assistant (Gemma 4 E4B) protected by seven guard agents, one per risk: a Node.js `preGeneration` hook asks them in parallel without the history, the assistant refuses or answers according to their verdicts; `displayHooks: true`; measured results | | 155 | | `demos/guards/` | an assistant (Gemma 4 E4B) protected by seven guard agents, one per risk: a Node.js `preGeneration` hook asks them in parallel without the history, the assistant refuses or answers according to their verdicts; `displayHooks: true`; measured results | |
| 156 | +| `demos/unload-models/` | a coordinator (Qwen3.5 9B) hands programming questions to an expert (Qwen3.5 4B) with `ask_agent`, a `preAsk` hook unloads the coordinator's own model before the expert starts and a `postAsk` hook unloads the expert's once it has answered — the two models are never loaded together; `/unload` and `unload_model` on request; llmman, measured results | | ||
| 155 | | `demos/declared-tools/` | an ops desk with no `bash` and no skills, acting only through four tools declared under `tools:` — release, version bump, service logs, slow requests — covering every parameter type (string, enum, boolean, integer, number), a safe default (`dryRun: true`) and an optional argument; Mellum2 12B on DMR, measured results | | 157 | | `demos/declared-tools/` | an ops desk with no `bash` and no skills, acting only through four tools declared under `tools:` — release, version bump, service logs, slow requests — covering every parameter type (string, enum, boolean, integer, number), a safe default (`dryRun: true`) and an optional argument; Mellum2 12B on DMR, measured results | |
| 156 | 158 | ||
| 157 | ## Learn more | 159 | ## Learn more |
modified
demos/guards/README.en.md +4 -0 | @@ -30,6 +30,10 @@ The hook runs the guards with the same `mm` as the assistant: mini-me exports it | ||
| 30 | 30 | |
| 31 | 31 | ## How it works |
| 32 | 32 | |
| 33 | + | |
| 34 | + | |
| 35 | +Editable source: `guards-flow.drawio` (draw.io); `guards-flow.svg` is the same picture. | |
| 36 | + | |
| 33 | 37 | ```text |
| 34 | 38 | user's question |
| 35 | 39 | │ |
| @@ -30,6 +30,10 @@ The hook runs the guards with the same `mm` as the assistant: mini-me exports it | |||
| 30 | 30 | ||
| 31 | ## How it works | 31 | ## How it works |
| 32 | 32 | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +Editable source: `guards-flow.drawio` (draw.io); `guards-flow.svg` is the same picture. | ||
| 36 | + | ||
| 33 | ```text | 37 | ```text |
| 34 | user's question | 38 | user's question |
| 35 | │ | 39 | │ |
modified
demos/guards/README.fr.md +4 -0 | @@ -30,6 +30,10 @@ Le hook lance les gardes avec le même `mm` que l'assistant : mini-me exporte so | ||
| 30 | 30 | |
| 31 | 31 | ## Comment ça marche |
| 32 | 32 | |
| 33 | + | |
| 34 | + | |
| 35 | +Source modifiable : `guards-flow.drawio` (draw.io) ; `guards-flow.svg` est la même image. | |
| 36 | + | |
| 33 | 37 | ```text |
| 34 | 38 | question de l'utilisateur |
| 35 | 39 | │ |
| @@ -30,6 +30,10 @@ Le hook lance les gardes avec le même `mm` que l'assistant : mini-me exporte so | |||
| 30 | 30 | ||
| 31 | ## Comment ça marche | 31 | ## Comment ça marche |
| 32 | 32 | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +Source modifiable : `guards-flow.drawio` (draw.io) ; `guards-flow.svg` est la même image. | ||
| 36 | + | ||
| 33 | ```text | 37 | ```text |
| 34 | question de l'utilisateur | 38 | question de l'utilisateur |
| 35 | │ | 39 | │ |
added
demos/guards/guards-flow.drawio +30 -0 | new file mode 100644 | ||
| @@ -0,0 +1,30 @@ | ||
| 1 | +<mxfile host="app.diagrams.net" agent="drawio-fancy-diagram-skill" version="24.7.17" type="device"> | |
| 2 | + <diagram name="demos/guards" id="guards-flow"> | |
| 3 | + <mxGraphModel dx="1400" dy="800" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="680" math="0" shadow="0"> | |
| 4 | + <root> | |
| 5 | + <mxCell id="0"/> | |
| 6 | + <mxCell id="1" parent="0"/> | |
| 7 | + <mxCell id="bg" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;gradientColor=#EEF2FA;strokeColor=none;" vertex="1" parent="1"><mxGeometry x="0" y="0" width="1200" height="680" as="geometry"/></mxCell> | |
| 8 | + <mxCell id="title" value="demos/guards — every question screened first" style="text;html=1;align=center;verticalAlign=middle;fontSize=24;fontStyle=1;fontColor=#1A237E;" vertex="1" parent="1"><mxGeometry x="200" y="18" width="800" height="30" as="geometry"/></mxCell> | |
| 9 | + <mxCell id="subtitle" value="a preGeneration hook asks seven guard agents, the question alone, before the assistant answers" style="text;html=1;align=center;verticalAlign=middle;fontSize=13;fontStyle=2;fontColor=#546E7A;" vertex="1" parent="1"><mxGeometry x="150" y="48" width="900" height="20" as="geometry"/></mxCell> | |
| 10 | + <mxCell id="user" value="🧑<br><b>User</b><br><font style="font-size:11px">asks a question<br>terminal · -tui · editor</font>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E3F2FD;gradientColor=#BBDEFB;strokeColor=#1565C0;strokeWidth=2;shadow=1;fontSize=15;fontColor=#0D47A1;" vertex="1" parent="1"><mxGeometry x="40" y="140" width="190" height="120" as="geometry"/></mxCell> | |
| 11 | + <mxCell id="hook" value="🔍<br><b>preGeneration hook</b><br><font style="font-size:11px">hooks/guards.js<br>no verdict → stop (fail closed)</font>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=15;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="140" width="250" height="120" as="geometry"/></mxCell> | |
| 12 | + <mxCell id="assistant" value="🤖<br><b>Assistant</b><br><font style="font-size:11px">Gemma 4 E4B · no tools<br>all No → answers · a Yes → refuses</font>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#F3E5F5;gradientColor=#E1BEE7;strokeColor=#6A1B9A;strokeWidth=3;shadow=1;fontSize=15;fontColor=#4A148C;" vertex="1" parent="1"><mxGeometry x="900" y="140" width="260" height="120" as="geometry"/></mxCell> | |
| 13 | + <mxCell id="badge" value="★ decides" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=none;fontSize=11;fontStyle=1;fontColor=#FFFFFF;" vertex="1" parent="1"><mxGeometry x="1060" y="128" width="88" height="24" as="geometry"/></mxCell> | |
| 14 | + <mxCell id="guards" value="7 guard agents · Granite Guardian 3.2 3B · in parallel, 1.2 s in total" style="rounded=1;arcSize=4;whiteSpace=wrap;html=1;fillColor=#FAFAFA;strokeColor=#90A4AE;strokeWidth=2;dashed=1;verticalAlign=top;align=center;fontSize=13;fontStyle=3;fontColor=#607D8B;spacingTop=8;" vertex="1" parent="1"><mxGeometry x="200" y="330" width="800" height="300" as="geometry"/></mxCell> | |
| 15 | + <mxCell id="g0" value="<b>harm</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="236" y="380" width="170" height="64" as="geometry"/></mxCell> | |
| 16 | + <mxCell id="g1" value="<b>jailbreak</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="422" y="380" width="170" height="64" as="geometry"/></mxCell> | |
| 17 | + <mxCell id="g2" value="<b>violence</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="608" y="380" width="170" height="64" as="geometry"/></mxCell> | |
| 18 | + <mxCell id="g3" value="<b>unethical_behavior</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="794" y="380" width="170" height="64" as="geometry"/></mxCell> | |
| 19 | + <mxCell id="g4" value="<b>social_bias</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="329" y="460" width="170" height="64" as="geometry"/></mxCell> | |
| 20 | + <mxCell id="g5" value="<b>profanity</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="515" y="460" width="170" height="64" as="geometry"/></mxCell> | |
| 21 | + <mxCell id="g6" value="<b>sexual_content</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="701" y="460" width="170" height="64" as="geometry"/></mxCell> | |
| 22 | + <mxCell id="notes" value="each guard: mm -config guards/&lt;risk&gt;.yaml -p="&lt;question&gt;"<br>system: &lt;risk name&gt; · a fresh conversation, never the history<br><i><font color="#78909C" style="font-size:11px">answers "Yes" or "No" + &lt;confidence&gt;</font></i>" style="text;html=1;align=center;verticalAlign=middle;fontSize=12;fontColor=#455A64;" vertex="1" parent="1"><mxGeometry x="250" y="550" width="700" height="70" as="geometry"/></mxCell> | |
| 23 | + <mxCell id="e1" value="question" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;fontSize=12;fontStyle=1;fontColor=#263238;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="user" target="hook"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell> | |
| 24 | + <mxCell id="e2" value="question + safety report<br><font style="font-size:10px;font-weight:normal" color="#607D8B">[context added by a hook]</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;fontSize=12;fontStyle=1;fontColor=#263238;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="hook" target="assistant"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell> | |
| 25 | + <mxCell id="e3" value="question alone<br><font style="font-size:10px;font-weight:normal" color="#607D8B">← Yes / No per risk</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;fontSize=12;fontStyle=1;fontColor=#263238;labelBackgroundColor=#FFFFFF;startArrow=block;startFill=1;align=left;spacingLeft=14;" edge="1" parent="1" source="hook" target="guards"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell> | |
| 26 | + <mxCell id="e4" value="answer · or refusal" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;fontSize=12;fontStyle=1;fontColor=#263238;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=0;entryX=0.5;entryY=0;" edge="1" parent="1" source="assistant" target="user"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="1030" y="100"/><mxPoint x="135" y="100"/></Array></mxGeometry></mxCell> | |
| 27 | + </root> | |
| 28 | + </mxGraphModel> | |
| 29 | + </diagram> | |
| 30 | +</mxfile> | |
| new file mode 100644 | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | +<mxfile host="app.diagrams.net" agent="drawio-fancy-diagram-skill" version="24.7.17" type="device"> | ||
| 2 | + <diagram name="demos/guards" id="guards-flow"> | ||
| 3 | + <mxGraphModel dx="1400" dy="800" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="680" math="0" shadow="0"> | ||
| 4 | + <root> | ||
| 5 | + <mxCell id="0"/> | ||
| 6 | + <mxCell id="1" parent="0"/> | ||
| 7 | + <mxCell id="bg" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#FFFFFF;gradientColor=#EEF2FA;strokeColor=none;" vertex="1" parent="1"><mxGeometry x="0" y="0" width="1200" height="680" as="geometry"/></mxCell> | ||
| 8 | + <mxCell id="title" value="demos/guards — every question screened first" style="text;html=1;align=center;verticalAlign=middle;fontSize=24;fontStyle=1;fontColor=#1A237E;" vertex="1" parent="1"><mxGeometry x="200" y="18" width="800" height="30" as="geometry"/></mxCell> | ||
| 9 | + <mxCell id="subtitle" value="a preGeneration hook asks seven guard agents, the question alone, before the assistant answers" style="text;html=1;align=center;verticalAlign=middle;fontSize=13;fontStyle=2;fontColor=#546E7A;" vertex="1" parent="1"><mxGeometry x="150" y="48" width="900" height="20" as="geometry"/></mxCell> | ||
| 10 | + <mxCell id="user" value="🧑<br><b>User</b><br><font style="font-size:11px">asks a question<br>terminal · -tui · editor</font>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E3F2FD;gradientColor=#BBDEFB;strokeColor=#1565C0;strokeWidth=2;shadow=1;fontSize=15;fontColor=#0D47A1;" vertex="1" parent="1"><mxGeometry x="40" y="140" width="190" height="120" as="geometry"/></mxCell> | ||
| 11 | + <mxCell id="hook" value="🔍<br><b>preGeneration hook</b><br><font style="font-size:11px">hooks/guards.js<br>no verdict → stop (fail closed)</font>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#E1F5FE;gradientColor=#B3E5FC;strokeColor=#0277BD;strokeWidth=2;shadow=1;fontSize=15;fontColor=#01579B;" vertex="1" parent="1"><mxGeometry x="330" y="140" width="250" height="120" as="geometry"/></mxCell> | ||
| 12 | + <mxCell id="assistant" value="🤖<br><b>Assistant</b><br><font style="font-size:11px">Gemma 4 E4B · no tools<br>all No → answers · a Yes → refuses</font>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#F3E5F5;gradientColor=#E1BEE7;strokeColor=#6A1B9A;strokeWidth=3;shadow=1;fontSize=15;fontColor=#4A148C;" vertex="1" parent="1"><mxGeometry x="900" y="140" width="260" height="120" as="geometry"/></mxCell> | ||
| 13 | + <mxCell id="badge" value="★ decides" style="rounded=1;arcSize=50;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=none;fontSize=11;fontStyle=1;fontColor=#FFFFFF;" vertex="1" parent="1"><mxGeometry x="1060" y="128" width="88" height="24" as="geometry"/></mxCell> | ||
| 14 | + <mxCell id="guards" value="7 guard agents · Granite Guardian 3.2 3B · in parallel, 1.2 s in total" style="rounded=1;arcSize=4;whiteSpace=wrap;html=1;fillColor=#FAFAFA;strokeColor=#90A4AE;strokeWidth=2;dashed=1;verticalAlign=top;align=center;fontSize=13;fontStyle=3;fontColor=#607D8B;spacingTop=8;" vertex="1" parent="1"><mxGeometry x="200" y="330" width="800" height="300" as="geometry"/></mxCell> | ||
| 15 | + <mxCell id="g0" value="<b>harm</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="236" y="380" width="170" height="64" as="geometry"/></mxCell> | ||
| 16 | + <mxCell id="g1" value="<b>jailbreak</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="422" y="380" width="170" height="64" as="geometry"/></mxCell> | ||
| 17 | + <mxCell id="g2" value="<b>violence</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="608" y="380" width="170" height="64" as="geometry"/></mxCell> | ||
| 18 | + <mxCell id="g3" value="<b>unethical_behavior</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="794" y="380" width="170" height="64" as="geometry"/></mxCell> | ||
| 19 | + <mxCell id="g4" value="<b>social_bias</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="329" y="460" width="170" height="64" as="geometry"/></mxCell> | ||
| 20 | + <mxCell id="g5" value="<b>profanity</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="515" y="460" width="170" height="64" as="geometry"/></mxCell> | ||
| 21 | + <mxCell id="g6" value="<b>sexual_content</b>" style="rounded=1;arcSize=14;whiteSpace=wrap;html=1;fillColor=#ECEFF1;gradientColor=#CFD8DC;strokeColor=#455A64;strokeWidth=2;shadow=1;fontSize=14;fontColor=#263238;" vertex="1" parent="1"><mxGeometry x="701" y="460" width="170" height="64" as="geometry"/></mxCell> | ||
| 22 | + <mxCell id="notes" value="each guard: mm -config guards/&lt;risk&gt;.yaml -p="&lt;question&gt;"<br>system: &lt;risk name&gt; · a fresh conversation, never the history<br><i><font color="#78909C" style="font-size:11px">answers "Yes" or "No" + &lt;confidence&gt;</font></i>" style="text;html=1;align=center;verticalAlign=middle;fontSize=12;fontColor=#455A64;" vertex="1" parent="1"><mxGeometry x="250" y="550" width="700" height="70" as="geometry"/></mxCell> | ||
| 23 | + <mxCell id="e1" value="question" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;fontSize=12;fontStyle=1;fontColor=#263238;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="user" target="hook"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell> | ||
| 24 | + <mxCell id="e2" value="question + safety report<br><font style="font-size:10px;font-weight:normal" color="#607D8B">[context added by a hook]</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;fontSize=12;fontStyle=1;fontColor=#263238;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="hook" target="assistant"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell> | ||
| 25 | + <mxCell id="e3" value="question alone<br><font style="font-size:10px;font-weight:normal" color="#607D8B">← Yes / No per risk</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;fontSize=12;fontStyle=1;fontColor=#263238;labelBackgroundColor=#FFFFFF;startArrow=block;startFill=1;align=left;spacingLeft=14;" edge="1" parent="1" source="hook" target="guards"><mxGeometry relative="1" as="geometry"></mxGeometry></mxCell> | ||
| 26 | + <mxCell id="e4" value="answer · or refusal" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#37474F;strokeWidth=2;endArrow=block;endFill=1;fontSize=12;fontStyle=1;fontColor=#263238;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=0;entryX=0.5;entryY=0;" edge="1" parent="1" source="assistant" target="user"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="1030" y="100"/><mxPoint x="135" y="100"/></Array></mxGeometry></mxCell> | ||
| 27 | + </root> | ||
| 28 | + </mxGraphModel> | ||
| 29 | + </diagram> | ||
| 30 | +</mxfile> | ||
added
demos/guards/guards-flow.png +0 -0 | new file mode 100644 | ||
| Binary files /dev/null and b/demos/guards/guards-flow.png differ | ||
| new file mode 100644 | |||
| Binary files /dev/null and b/demos/guards/guards-flow.png differ | Binary files /dev/null and b/demos/guards/guards-flow.png differ | ||
added
demos/guards/guards-flow.svg +117 -0 | new file mode 100644 | ||
| @@ -0,0 +1,117 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="680" viewBox="0 0 1200 680" font-family="DejaVu Sans, Verdana, sans-serif"> | |
| 2 | + <defs> | |
| 3 | + <linearGradient id="gBg" x1="0" y1="0" x2="0" y2="1"> | |
| 4 | + <stop offset="0%" stop-color="#FFFFFF"/> | |
| 5 | + <stop offset="100%" stop-color="#EEF2FA"/> | |
| 6 | + </linearGradient> | |
| 7 | + <linearGradient id="gBlue" x1="0" y1="0" x2="0" y2="1"> | |
| 8 | + <stop offset="0%" stop-color="#E3F2FD"/> | |
| 9 | + <stop offset="100%" stop-color="#BBDEFB"/> | |
| 10 | + </linearGradient> | |
| 11 | + <linearGradient id="gBlue2" x1="0" y1="0" x2="0" y2="1"> | |
| 12 | + <stop offset="0%" stop-color="#E1F5FE"/> | |
| 13 | + <stop offset="100%" stop-color="#B3E5FC"/> | |
| 14 | + </linearGradient> | |
| 15 | + <linearGradient id="gPurple" x1="0" y1="0" x2="0" y2="1"> | |
| 16 | + <stop offset="0%" stop-color="#F3E5F5"/> | |
| 17 | + <stop offset="100%" stop-color="#E1BEE7"/> | |
| 18 | + </linearGradient> | |
| 19 | + <linearGradient id="gGrey" x1="0" y1="0" x2="0" y2="1"> | |
| 20 | + <stop offset="0%" stop-color="#ECEFF1"/> | |
| 21 | + <stop offset="100%" stop-color="#CFD8DC"/> | |
| 22 | + </linearGradient> | |
| 23 | + <filter id="shadow" x="-30%" y="-30%" width="160%" height="160%"> | |
| 24 | + <feDropShadow dx="2" dy="3" stdDeviation="4" flood-color="#000000" flood-opacity="0.25"/> | |
| 25 | + </filter> | |
| 26 | + <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"> | |
| 27 | + <path d="M0,0 L10,5 L0,10 z" fill="#37474F"/> | |
| 28 | + </marker> | |
| 29 | + </defs> | |
| 30 | + | |
| 31 | + <rect x="0" y="0" width="1200" height="680" fill="url(#gBg)"/> | |
| 32 | + | |
| 33 | + <text x="600" y="38" text-anchor="middle" font-size="25" font-weight="700" fill="#1A237E">demos/guards — every question screened first</text> | |
| 34 | + <text x="600" y="61" text-anchor="middle" font-size="14" font-style="italic" fill="#546E7A">a preGeneration hook asks seven guard agents, the question alone, before the assistant answers</text> | |
| 35 | + | |
| 36 | + <!-- Answer: Assistant -> User (above the row) --> | |
| 37 | + <polyline points="1030,140 1030,100 135,100 135,138" fill="none" stroke="#37474F" stroke-width="2" marker-end="url(#arrow)"/> | |
| 38 | + <rect x="505" y="88" width="190" height="24" rx="4" fill="#FFFFFF"/> | |
| 39 | + <text x="600" y="105" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">answer · or refusal</text> | |
| 40 | + | |
| 41 | + <!-- User --> | |
| 42 | + <g filter="url(#shadow)"> | |
| 43 | + <rect x="40" y="140" width="190" height="120" rx="16" fill="url(#gBlue)" stroke="#1565C0" stroke-width="2"/> | |
| 44 | + </g> | |
| 45 | + <text x="135" y="182" text-anchor="middle" font-size="24">🧑</text> | |
| 46 | + <text x="135" y="210" text-anchor="middle" font-size="16" font-weight="700" fill="#0D47A1">User</text> | |
| 47 | + <text x="135" y="230" text-anchor="middle" font-size="11.5" fill="#0D47A1">asks a question</text> | |
| 48 | + <text x="135" y="246" text-anchor="middle" font-size="11.5" fill="#0D47A1">terminal · -tui · editor</text> | |
| 49 | + | |
| 50 | + <!-- User -> Hook --> | |
| 51 | + <line x1="230" y1="200" x2="328" y2="200" stroke="#37474F" stroke-width="2" marker-end="url(#arrow)"/> | |
| 52 | + <rect x="243" y="190" width="72" height="20" rx="4" fill="#FFFFFF"/> | |
| 53 | + <text x="279" y="204" text-anchor="middle" font-size="12" font-weight="700" fill="#263238">question</text> | |
| 54 | + | |
| 55 | + <!-- Hook --> | |
| 56 | + <g filter="url(#shadow)"> | |
| 57 | + <rect x="330" y="140" width="250" height="120" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/> | |
| 58 | + </g> | |
| 59 | + <text x="455" y="178" text-anchor="middle" font-size="22">🔍</text> | |
| 60 | + <text x="455" y="204" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">preGeneration hook</text> | |
| 61 | + <text x="455" y="223" text-anchor="middle" font-size="11.5" fill="#01579B">hooks/guards.js</text> | |
| 62 | + <text x="455" y="240" text-anchor="middle" font-size="11.5" fill="#01579B">no verdict → stop (fail closed)</text> | |
| 63 | + | |
| 64 | + <!-- Hook -> Assistant --> | |
| 65 | + <line x1="580" y1="200" x2="898" y2="200" stroke="#37474F" stroke-width="2" marker-end="url(#arrow)"/> | |
| 66 | + <rect x="632" y="180" width="216" height="40" rx="4" fill="#FFFFFF"/> | |
| 67 | + <text x="740" y="196" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">question + safety report</text> | |
| 68 | + <text x="740" y="212" text-anchor="middle" font-size="10.5" fill="#607D8B">[context added by a hook]</text> | |
| 69 | + | |
| 70 | + <!-- Assistant (highlighted) --> | |
| 71 | + <g filter="url(#shadow)"> | |
| 72 | + <rect x="900" y="140" width="260" height="120" rx="16" fill="url(#gPurple)" stroke="#6A1B9A" stroke-width="3"/> | |
| 73 | + </g> | |
| 74 | + <rect x="1060" y="128" width="88" height="24" rx="12" fill="#6A1B9A"/> | |
| 75 | + <text x="1104" y="144.5" text-anchor="middle" font-size="11" font-weight="700" fill="#FFFFFF">★ decides</text> | |
| 76 | + <text x="1030" y="180" text-anchor="middle" font-size="22">🤖</text> | |
| 77 | + <text x="1030" y="206" text-anchor="middle" font-size="16" font-weight="700" fill="#4A148C">Assistant</text> | |
| 78 | + <text x="1030" y="225" text-anchor="middle" font-size="11.5" fill="#4A148C">Gemma 4 E4B · no tools</text> | |
| 79 | + <text x="1030" y="242" text-anchor="middle" font-size="11.5" fill="#4A148C">all No → answers · a Yes → refuses</text> | |
| 80 | + | |
| 81 | + <!-- Hook <-> guards --> | |
| 82 | + <line x1="455" y1="262" x2="455" y2="328" stroke="#37474F" stroke-width="2" marker-start="url(#arrow)" marker-end="url(#arrow)"/> | |
| 83 | + <rect x="468" y="274" width="170" height="40" rx="4" fill="#FFFFFF"/> | |
| 84 | + <text x="474" y="290" font-size="12.5" font-weight="700" fill="#263238">question alone</text> | |
| 85 | + <text x="474" y="306" font-size="10.5" fill="#607D8B">← Yes / No per risk</text> | |
| 86 | + | |
| 87 | + <!-- Guards container --> | |
| 88 | + <rect x="200" y="330" width="800" height="300" rx="10" fill="#FAFAFA" stroke="#90A4AE" stroke-width="2" stroke-dasharray="7,5"/> | |
| 89 | + <text x="600" y="355" text-anchor="middle" font-size="13" font-style="italic" font-weight="700" fill="#607D8B">7 guard agents · Granite Guardian 3.2 3B · in parallel, 1.2 s in total</text> | |
| 90 | + | |
| 91 | + <!-- Row 1 --> | |
| 92 | + <g filter="url(#shadow)"> | |
| 93 | + <rect x="236" y="380" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | |
| 94 | + <rect x="422" y="380" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | |
| 95 | + <rect x="608" y="380" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | |
| 96 | + <rect x="794" y="380" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | |
| 97 | + </g> | |
| 98 | + <text x="321" y="417" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">harm</text> | |
| 99 | + <text x="507" y="417" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">jailbreak</text> | |
| 100 | + <text x="693" y="417" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">violence</text> | |
| 101 | + <text x="879" y="417" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">unethical_behavior</text> | |
| 102 | + | |
| 103 | + <!-- Row 2 --> | |
| 104 | + <g filter="url(#shadow)"> | |
| 105 | + <rect x="329" y="460" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | |
| 106 | + <rect x="515" y="460" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | |
| 107 | + <rect x="701" y="460" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | |
| 108 | + </g> | |
| 109 | + <text x="414" y="497" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">social_bias</text> | |
| 110 | + <text x="600" y="497" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">profanity</text> | |
| 111 | + <text x="786" y="497" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">sexual_content</text> | |
| 112 | + | |
| 113 | + <!-- Container notes --> | |
| 114 | + <text x="600" y="566" text-anchor="middle" font-size="12" fill="#455A64">each guard: mm -config guards/<risk>.yaml -p="<question>"</text> | |
| 115 | + <text x="600" y="586" text-anchor="middle" font-size="12" fill="#455A64">system: <risk name> · a fresh conversation, never the history</text> | |
| 116 | + <text x="600" y="610" text-anchor="middle" font-size="11" font-style="italic" fill="#78909C">answers "Yes" or "No" + <confidence></text> | |
| 117 | +</svg> | |
| new file mode 100644 | |||
| @@ -0,0 +1,117 @@ | |||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="680" viewBox="0 0 1200 680" font-family="DejaVu Sans, Verdana, sans-serif"> | ||
| 2 | + <defs> | ||
| 3 | + <linearGradient id="gBg" x1="0" y1="0" x2="0" y2="1"> | ||
| 4 | + <stop offset="0%" stop-color="#FFFFFF"/> | ||
| 5 | + <stop offset="100%" stop-color="#EEF2FA"/> | ||
| 6 | + </linearGradient> | ||
| 7 | + <linearGradient id="gBlue" x1="0" y1="0" x2="0" y2="1"> | ||
| 8 | + <stop offset="0%" stop-color="#E3F2FD"/> | ||
| 9 | + <stop offset="100%" stop-color="#BBDEFB"/> | ||
| 10 | + </linearGradient> | ||
| 11 | + <linearGradient id="gBlue2" x1="0" y1="0" x2="0" y2="1"> | ||
| 12 | + <stop offset="0%" stop-color="#E1F5FE"/> | ||
| 13 | + <stop offset="100%" stop-color="#B3E5FC"/> | ||
| 14 | + </linearGradient> | ||
| 15 | + <linearGradient id="gPurple" x1="0" y1="0" x2="0" y2="1"> | ||
| 16 | + <stop offset="0%" stop-color="#F3E5F5"/> | ||
| 17 | + <stop offset="100%" stop-color="#E1BEE7"/> | ||
| 18 | + </linearGradient> | ||
| 19 | + <linearGradient id="gGrey" x1="0" y1="0" x2="0" y2="1"> | ||
| 20 | + <stop offset="0%" stop-color="#ECEFF1"/> | ||
| 21 | + <stop offset="100%" stop-color="#CFD8DC"/> | ||
| 22 | + </linearGradient> | ||
| 23 | + <filter id="shadow" x="-30%" y="-30%" width="160%" height="160%"> | ||
| 24 | + <feDropShadow dx="2" dy="3" stdDeviation="4" flood-color="#000000" flood-opacity="0.25"/> | ||
| 25 | + </filter> | ||
| 26 | + <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"> | ||
| 27 | + <path d="M0,0 L10,5 L0,10 z" fill="#37474F"/> | ||
| 28 | + </marker> | ||
| 29 | + </defs> | ||
| 30 | + | ||
| 31 | + <rect x="0" y="0" width="1200" height="680" fill="url(#gBg)"/> | ||
| 32 | + | ||
| 33 | + <text x="600" y="38" text-anchor="middle" font-size="25" font-weight="700" fill="#1A237E">demos/guards — every question screened first</text> | ||
| 34 | + <text x="600" y="61" text-anchor="middle" font-size="14" font-style="italic" fill="#546E7A">a preGeneration hook asks seven guard agents, the question alone, before the assistant answers</text> | ||
| 35 | + | ||
| 36 | + <!-- Answer: Assistant -> User (above the row) --> | ||
| 37 | + <polyline points="1030,140 1030,100 135,100 135,138" fill="none" stroke="#37474F" stroke-width="2" marker-end="url(#arrow)"/> | ||
| 38 | + <rect x="505" y="88" width="190" height="24" rx="4" fill="#FFFFFF"/> | ||
| 39 | + <text x="600" y="105" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">answer · or refusal</text> | ||
| 40 | + | ||
| 41 | + <!-- User --> | ||
| 42 | + <g filter="url(#shadow)"> | ||
| 43 | + <rect x="40" y="140" width="190" height="120" rx="16" fill="url(#gBlue)" stroke="#1565C0" stroke-width="2"/> | ||
| 44 | + </g> | ||
| 45 | + <text x="135" y="182" text-anchor="middle" font-size="24">🧑</text> | ||
| 46 | + <text x="135" y="210" text-anchor="middle" font-size="16" font-weight="700" fill="#0D47A1">User</text> | ||
| 47 | + <text x="135" y="230" text-anchor="middle" font-size="11.5" fill="#0D47A1">asks a question</text> | ||
| 48 | + <text x="135" y="246" text-anchor="middle" font-size="11.5" fill="#0D47A1">terminal · -tui · editor</text> | ||
| 49 | + | ||
| 50 | + <!-- User -> Hook --> | ||
| 51 | + <line x1="230" y1="200" x2="328" y2="200" stroke="#37474F" stroke-width="2" marker-end="url(#arrow)"/> | ||
| 52 | + <rect x="243" y="190" width="72" height="20" rx="4" fill="#FFFFFF"/> | ||
| 53 | + <text x="279" y="204" text-anchor="middle" font-size="12" font-weight="700" fill="#263238">question</text> | ||
| 54 | + | ||
| 55 | + <!-- Hook --> | ||
| 56 | + <g filter="url(#shadow)"> | ||
| 57 | + <rect x="330" y="140" width="250" height="120" rx="16" fill="url(#gBlue2)" stroke="#0277BD" stroke-width="2"/> | ||
| 58 | + </g> | ||
| 59 | + <text x="455" y="178" text-anchor="middle" font-size="22">🔍</text> | ||
| 60 | + <text x="455" y="204" text-anchor="middle" font-size="16" font-weight="700" fill="#01579B">preGeneration hook</text> | ||
| 61 | + <text x="455" y="223" text-anchor="middle" font-size="11.5" fill="#01579B">hooks/guards.js</text> | ||
| 62 | + <text x="455" y="240" text-anchor="middle" font-size="11.5" fill="#01579B">no verdict → stop (fail closed)</text> | ||
| 63 | + | ||
| 64 | + <!-- Hook -> Assistant --> | ||
| 65 | + <line x1="580" y1="200" x2="898" y2="200" stroke="#37474F" stroke-width="2" marker-end="url(#arrow)"/> | ||
| 66 | + <rect x="632" y="180" width="216" height="40" rx="4" fill="#FFFFFF"/> | ||
| 67 | + <text x="740" y="196" text-anchor="middle" font-size="12.5" font-weight="700" fill="#263238">question + safety report</text> | ||
| 68 | + <text x="740" y="212" text-anchor="middle" font-size="10.5" fill="#607D8B">[context added by a hook]</text> | ||
| 69 | + | ||
| 70 | + <!-- Assistant (highlighted) --> | ||
| 71 | + <g filter="url(#shadow)"> | ||
| 72 | + <rect x="900" y="140" width="260" height="120" rx="16" fill="url(#gPurple)" stroke="#6A1B9A" stroke-width="3"/> | ||
| 73 | + </g> | ||
| 74 | + <rect x="1060" y="128" width="88" height="24" rx="12" fill="#6A1B9A"/> | ||
| 75 | + <text x="1104" y="144.5" text-anchor="middle" font-size="11" font-weight="700" fill="#FFFFFF">★ decides</text> | ||
| 76 | + <text x="1030" y="180" text-anchor="middle" font-size="22">🤖</text> | ||
| 77 | + <text x="1030" y="206" text-anchor="middle" font-size="16" font-weight="700" fill="#4A148C">Assistant</text> | ||
| 78 | + <text x="1030" y="225" text-anchor="middle" font-size="11.5" fill="#4A148C">Gemma 4 E4B · no tools</text> | ||
| 79 | + <text x="1030" y="242" text-anchor="middle" font-size="11.5" fill="#4A148C">all No → answers · a Yes → refuses</text> | ||
| 80 | + | ||
| 81 | + <!-- Hook <-> guards --> | ||
| 82 | + <line x1="455" y1="262" x2="455" y2="328" stroke="#37474F" stroke-width="2" marker-start="url(#arrow)" marker-end="url(#arrow)"/> | ||
| 83 | + <rect x="468" y="274" width="170" height="40" rx="4" fill="#FFFFFF"/> | ||
| 84 | + <text x="474" y="290" font-size="12.5" font-weight="700" fill="#263238">question alone</text> | ||
| 85 | + <text x="474" y="306" font-size="10.5" fill="#607D8B">← Yes / No per risk</text> | ||
| 86 | + | ||
| 87 | + <!-- Guards container --> | ||
| 88 | + <rect x="200" y="330" width="800" height="300" rx="10" fill="#FAFAFA" stroke="#90A4AE" stroke-width="2" stroke-dasharray="7,5"/> | ||
| 89 | + <text x="600" y="355" text-anchor="middle" font-size="13" font-style="italic" font-weight="700" fill="#607D8B">7 guard agents · Granite Guardian 3.2 3B · in parallel, 1.2 s in total</text> | ||
| 90 | + | ||
| 91 | + <!-- Row 1 --> | ||
| 92 | + <g filter="url(#shadow)"> | ||
| 93 | + <rect x="236" y="380" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | ||
| 94 | + <rect x="422" y="380" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | ||
| 95 | + <rect x="608" y="380" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | ||
| 96 | + <rect x="794" y="380" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | ||
| 97 | + </g> | ||
| 98 | + <text x="321" y="417" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">harm</text> | ||
| 99 | + <text x="507" y="417" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">jailbreak</text> | ||
| 100 | + <text x="693" y="417" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">violence</text> | ||
| 101 | + <text x="879" y="417" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">unethical_behavior</text> | ||
| 102 | + | ||
| 103 | + <!-- Row 2 --> | ||
| 104 | + <g filter="url(#shadow)"> | ||
| 105 | + <rect x="329" y="460" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | ||
| 106 | + <rect x="515" y="460" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | ||
| 107 | + <rect x="701" y="460" width="170" height="64" rx="14" fill="url(#gGrey)" stroke="#455A64" stroke-width="2"/> | ||
| 108 | + </g> | ||
| 109 | + <text x="414" y="497" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">social_bias</text> | ||
| 110 | + <text x="600" y="497" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">profanity</text> | ||
| 111 | + <text x="786" y="497" text-anchor="middle" font-size="14" font-weight="700" fill="#263238">sexual_content</text> | ||
| 112 | + | ||
| 113 | + <!-- Container notes --> | ||
| 114 | + <text x="600" y="566" text-anchor="middle" font-size="12" fill="#455A64">each guard: mm -config guards/<risk>.yaml -p="<question>"</text> | ||
| 115 | + <text x="600" y="586" text-anchor="middle" font-size="12" fill="#455A64">system: <risk name> · a fresh conversation, never the history</text> | ||
| 116 | + <text x="600" y="610" text-anchor="middle" font-size="11" font-style="italic" fill="#78909C">answers "Yes" or "No" + <confidence></text> | ||
| 117 | +</svg> | ||
added
demos/unload-models/README.en.md +86 -0 | new file mode 100644 | ||
| @@ -0,0 +1,86 @@ | ||
| 1 | +# mini-me demo: unloading models | |
| 2 | + | |
| 3 | +*[Version française](README.fr.md)* | |
| 4 | + | |
| 5 | +A coordinator hands each programming question to an expert, and the two models take turns in the server's memory without ever being loaded together: a `preAsk` hook unloads the coordinator's model right before the expert starts, and a `postAsk` hook unloads the expert's model once it has answered, before the coordinator is reloaded to read the answer. The models are served by [llmman](https://github.com/llmmanorg/llmman). | |
| 6 | + | |
| 7 | +## What is here | |
| 8 | + | |
| 9 | +| File | Role | | |
| 10 | +|---|---| | |
| 11 | +| `agent.yaml` | The **coordinator** (Qwen3.5 9B): `ask_agent` to the expert, then the answer. The two hooks. `/unload` and the `unload_model` tool work here too. `displayHooks: true` shows the hook runs. | | |
| 12 | +| `hooks/unload.js` | The hook (Node.js). `unload.js self` (`preAsk`): unloads the coordinator's own model right before mm starts the expert. `unload.js asked` (`postAsk`): unloads the expert's model once it has answered. | | |
| 13 | +| `agent.expert.yaml` | The **expert** (Qwen3.5 4B): answers programming questions, no tools at all (`unloadModels: false`). | | |
| 14 | + | |
| 15 | +## Run the demo | |
| 16 | + | |
| 17 | +```bash | |
| 18 | +llmman pull hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M | |
| 19 | +llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | |
| 20 | +cd demos/unload-models | |
| 21 | +mm -tui agent.yaml | |
| 22 | +``` | |
| 23 | + | |
| 24 | +Ask a programming question, then look at what the server holds: | |
| 25 | + | |
| 26 | +```bash | |
| 27 | +llmman ps # or: curl -s http://127.0.0.1:17434/api/ps | |
| 28 | +``` | |
| 29 | + | |
| 30 | +After the answer, only the coordinator's Qwen3.5 9B is loaded. In the terminal the hand-off shows as: | |
| 31 | + | |
| 32 | +```text | |
| 33 | +🗣 ask_agent: expert (llmman/hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M) — Explain what a closure is in Ja… | |
| 34 | + 🪝 1. preAsk expert · node "$MM_CONFIG_DIR/hooks/unload.js" self → continue | |
| 35 | + 🪝 2. postAsk expert · node "$MM_CONFIG_DIR/hooks/unload.js" asked → continue | |
| 36 | +``` | |
| 37 | + | |
| 38 | +By hand: `/unload` unloads the coordinator's own model, `/unload <model>` any other one of the same server — in the terminal, the window and an editor. Asking the coordinator to free the expert's memory ("Unload the expert's model.", "Libère la mémoire de l'expert.") makes it call `unload_model` on the expert's model (4/4 measured): its name is written in the prompt, as the model cannot guess how the server names a model. | |
| 39 | + | |
| 40 | +## How it works | |
| 41 | + | |
| 42 | +What the server holds during one question (measured with `/api/ps` twice a second): | |
| 43 | + | |
| 44 | +| Moment | Loaded | | |
| 45 | +|---|---| | |
| 46 | +| the coordinator reads the question and calls `ask_agent` | Qwen3.5 9B | | |
| 47 | +| the `preAsk` hook unloads the coordinator | — | | |
| 48 | +| the expert works | Qwen3.5 4B alone | | |
| 49 | +| the `postAsk` hook unloads the expert | — | | |
| 50 | +| the coordinator reads the answer (the server reloads it) | Qwen3.5 9B alone | | |
| 51 | + | |
| 52 | +**Why hooks, and not the model.** Both unloads must happen at a moment when the coordinator's model is not running: | |
| 53 | + | |
| 54 | +- *Before the expert*: the coordinator's model has just decided to call `ask_agent`, and it is needed again only to read the answer. The model cannot unload itself — `unload_model` on its own model lasts until its next step. | |
| 55 | +- *After the expert*: if the coordinator called `unload_model` on the expert's model, it would already be running again — reloaded beside the 4B — when it does so. The two models would be loaded together for the whole step. (That is what the previous version of this demo did; see below.) | |
| 56 | + | |
| 57 | +mm runs the `preAsk` hooks right before it starts the asked agent and the `postAsk` hooks right after its answer, before the asking agent reads it. `hooks/unload.js` reads the model to unload from its input — `"model": "llmman/<model>"` for `self`, `"ask": {"agentModel": "llmman/<model>"}` for `asked` — and sends `keep_alive: 0` to the server. `matcher: ^expert$` keeps both hooks to this hand-off. `onError: warn`: if an unload fails, the hand-off goes on. | |
| 58 | + | |
| 59 | +The hooks are also certain: a model can skip an instruction, a hook always runs. | |
| 60 | + | |
| 61 | +## Measured results (2026-10-18, `mm -p`, llmman) | |
| 62 | + | |
| 63 | +Coordinator Qwen3.5 9B, expert Qwen3.5 4B, both hooks on. "Never together" means `/api/ps` (sampled twice a second) never showed both models at once during the question: 9B → nothing → 4B → nothing → 9B. | |
| 64 | + | |
| 65 | +| Question | `ask_agent` | `preAsk` unloads the 9B | `postAsk` unloads the 4B | never together | Time | | |
| 66 | +|---|---|---|---|---|---| | |
| 67 | +| How do I reverse a string in Go? | ✓ | ✓ | ✓ | ✓ | 62 s | | |
| 68 | +| What is a closure in JavaScript? | ✓ | ✓ | ✓ | ✓ | 65 s | | |
| 69 | +| How do I read a file line by line in Python? | ✓ | ✓ | ✓ | ✓ | 49 s | | |
| 70 | +| Explain Rust ownership briefly. | ✓ | ✓ | ✓ | ✓ | 82 s | | |
| 71 | +| Comment trier un tableau en Java ? | ✓ | ✓ | ✓ | ✓ | 119 s | | |
| 72 | +| How do I make an HTTP GET request in Go? | ✓ | ✓ | ✓ | ✓ | 106 s | | |
| 73 | +| What does the yield keyword do in Python? (×3) | ✓ ✓ ✓ | ✓ ✓ ✓ | ✓ ✓ ✓ | ✓ ✓ ✓ | 57 s, 60 s, 85 s | | |
| 74 | + | |
| 75 | +9 out of 9. The French question is answered in French. A greeting ("Hello, who are you?") is answered in 2 s without any tool. The times include the loads: each question loads the 9B twice and the 4B once, and vary a lot from one run to the next. | |
| 76 | + | |
| 77 | +**The prompt.** Without the `unload_model` step, a short prompt ("1. Call ask_agent… 2. Then give the expert's answer") let the coordinator answer the `yield` question by itself, without the expert (1 time out of 7). Saying that `ask_agent` is ALWAYS the first action, "even for a question you know the answer to", gave the table above. | |
| 78 | + | |
| 79 | +**Earlier versions.** With the coordinator freeing the expert through `unload_model` (no `postAsk` hook), the 9B and the 4B were loaded together for ~18 s per question while the coordinator read the answer — 7/7 questions otherwise clean. Before that, with a MiniCPM5 2B coordinator and a Qwen3.5 9B expert, the coordinator skipped `unload_model` once in five. | |
| 80 | + | |
| 81 | +## Good to know | |
| 82 | + | |
| 83 | +- **`postAsk` runs after every hand-off to the expert**, answered, failed or cancelled: the expert's model is unloaded even when its turn failed. | |
| 84 | +- **The next question reloads the 4B**: a hand-off costs one load of each model, plus a reload of the coordinator. Keeping both loaded is faster, when the memory allows it. | |
| 85 | +- **llmman also unloads by itself** a model idle for five minutes (`keep_alive`); the hooks free the memory at once, which matters when the next model needs it right away. | |
| 86 | +- **Turn off `/unload` and `unload_model`** with `unloadModels: false`, as the expert does: an agent that only answers does not need one more tool. The hooks do not depend on that key. | |
| new file mode 100644 | |||
| @@ -0,0 +1,86 @@ | |||
| 1 | +# mini-me demo: unloading models | ||
| 2 | + | ||
| 3 | +*[Version française](README.fr.md)* | ||
| 4 | + | ||
| 5 | +A coordinator hands each programming question to an expert, and the two models take turns in the server's memory without ever being loaded together: a `preAsk` hook unloads the coordinator's model right before the expert starts, and a `postAsk` hook unloads the expert's model once it has answered, before the coordinator is reloaded to read the answer. The models are served by [llmman](https://github.com/llmmanorg/llmman). | ||
| 6 | + | ||
| 7 | +## What is here | ||
| 8 | + | ||
| 9 | +| File | Role | | ||
| 10 | +|---|---| | ||
| 11 | +| `agent.yaml` | The **coordinator** (Qwen3.5 9B): `ask_agent` to the expert, then the answer. The two hooks. `/unload` and the `unload_model` tool work here too. `displayHooks: true` shows the hook runs. | | ||
| 12 | +| `hooks/unload.js` | The hook (Node.js). `unload.js self` (`preAsk`): unloads the coordinator's own model right before mm starts the expert. `unload.js asked` (`postAsk`): unloads the expert's model once it has answered. | | ||
| 13 | +| `agent.expert.yaml` | The **expert** (Qwen3.5 4B): answers programming questions, no tools at all (`unloadModels: false`). | | ||
| 14 | + | ||
| 15 | +## Run the demo | ||
| 16 | + | ||
| 17 | +```bash | ||
| 18 | +llmman pull hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M | ||
| 19 | +llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | ||
| 20 | +cd demos/unload-models | ||
| 21 | +mm -tui agent.yaml | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +Ask a programming question, then look at what the server holds: | ||
| 25 | + | ||
| 26 | +```bash | ||
| 27 | +llmman ps # or: curl -s http://127.0.0.1:17434/api/ps | ||
| 28 | +``` | ||
| 29 | + | ||
| 30 | +After the answer, only the coordinator's Qwen3.5 9B is loaded. In the terminal the hand-off shows as: | ||
| 31 | + | ||
| 32 | +```text | ||
| 33 | +🗣 ask_agent: expert (llmman/hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M) — Explain what a closure is in Ja… | ||
| 34 | + 🪝 1. preAsk expert · node "$MM_CONFIG_DIR/hooks/unload.js" self → continue | ||
| 35 | + 🪝 2. postAsk expert · node "$MM_CONFIG_DIR/hooks/unload.js" asked → continue | ||
| 36 | +``` | ||
| 37 | + | ||
| 38 | +By hand: `/unload` unloads the coordinator's own model, `/unload <model>` any other one of the same server — in the terminal, the window and an editor. Asking the coordinator to free the expert's memory ("Unload the expert's model.", "Libère la mémoire de l'expert.") makes it call `unload_model` on the expert's model (4/4 measured): its name is written in the prompt, as the model cannot guess how the server names a model. | ||
| 39 | + | ||
| 40 | +## How it works | ||
| 41 | + | ||
| 42 | +What the server holds during one question (measured with `/api/ps` twice a second): | ||
| 43 | + | ||
| 44 | +| Moment | Loaded | | ||
| 45 | +|---|---| | ||
| 46 | +| the coordinator reads the question and calls `ask_agent` | Qwen3.5 9B | | ||
| 47 | +| the `preAsk` hook unloads the coordinator | — | | ||
| 48 | +| the expert works | Qwen3.5 4B alone | | ||
| 49 | +| the `postAsk` hook unloads the expert | — | | ||
| 50 | +| the coordinator reads the answer (the server reloads it) | Qwen3.5 9B alone | | ||
| 51 | + | ||
| 52 | +**Why hooks, and not the model.** Both unloads must happen at a moment when the coordinator's model is not running: | ||
| 53 | + | ||
| 54 | +- *Before the expert*: the coordinator's model has just decided to call `ask_agent`, and it is needed again only to read the answer. The model cannot unload itself — `unload_model` on its own model lasts until its next step. | ||
| 55 | +- *After the expert*: if the coordinator called `unload_model` on the expert's model, it would already be running again — reloaded beside the 4B — when it does so. The two models would be loaded together for the whole step. (That is what the previous version of this demo did; see below.) | ||
| 56 | + | ||
| 57 | +mm runs the `preAsk` hooks right before it starts the asked agent and the `postAsk` hooks right after its answer, before the asking agent reads it. `hooks/unload.js` reads the model to unload from its input — `"model": "llmman/<model>"` for `self`, `"ask": {"agentModel": "llmman/<model>"}` for `asked` — and sends `keep_alive: 0` to the server. `matcher: ^expert$` keeps both hooks to this hand-off. `onError: warn`: if an unload fails, the hand-off goes on. | ||
| 58 | + | ||
| 59 | +The hooks are also certain: a model can skip an instruction, a hook always runs. | ||
| 60 | + | ||
| 61 | +## Measured results (2026-10-18, `mm -p`, llmman) | ||
| 62 | + | ||
| 63 | +Coordinator Qwen3.5 9B, expert Qwen3.5 4B, both hooks on. "Never together" means `/api/ps` (sampled twice a second) never showed both models at once during the question: 9B → nothing → 4B → nothing → 9B. | ||
| 64 | + | ||
| 65 | +| Question | `ask_agent` | `preAsk` unloads the 9B | `postAsk` unloads the 4B | never together | Time | | ||
| 66 | +|---|---|---|---|---|---| | ||
| 67 | +| How do I reverse a string in Go? | ✓ | ✓ | ✓ | ✓ | 62 s | | ||
| 68 | +| What is a closure in JavaScript? | ✓ | ✓ | ✓ | ✓ | 65 s | | ||
| 69 | +| How do I read a file line by line in Python? | ✓ | ✓ | ✓ | ✓ | 49 s | | ||
| 70 | +| Explain Rust ownership briefly. | ✓ | ✓ | ✓ | ✓ | 82 s | | ||
| 71 | +| Comment trier un tableau en Java ? | ✓ | ✓ | ✓ | ✓ | 119 s | | ||
| 72 | +| How do I make an HTTP GET request in Go? | ✓ | ✓ | ✓ | ✓ | 106 s | | ||
| 73 | +| What does the yield keyword do in Python? (×3) | ✓ ✓ ✓ | ✓ ✓ ✓ | ✓ ✓ ✓ | ✓ ✓ ✓ | 57 s, 60 s, 85 s | | ||
| 74 | + | ||
| 75 | +9 out of 9. The French question is answered in French. A greeting ("Hello, who are you?") is answered in 2 s without any tool. The times include the loads: each question loads the 9B twice and the 4B once, and vary a lot from one run to the next. | ||
| 76 | + | ||
| 77 | +**The prompt.** Without the `unload_model` step, a short prompt ("1. Call ask_agent… 2. Then give the expert's answer") let the coordinator answer the `yield` question by itself, without the expert (1 time out of 7). Saying that `ask_agent` is ALWAYS the first action, "even for a question you know the answer to", gave the table above. | ||
| 78 | + | ||
| 79 | +**Earlier versions.** With the coordinator freeing the expert through `unload_model` (no `postAsk` hook), the 9B and the 4B were loaded together for ~18 s per question while the coordinator read the answer — 7/7 questions otherwise clean. Before that, with a MiniCPM5 2B coordinator and a Qwen3.5 9B expert, the coordinator skipped `unload_model` once in five. | ||
| 80 | + | ||
| 81 | +## Good to know | ||
| 82 | + | ||
| 83 | +- **`postAsk` runs after every hand-off to the expert**, answered, failed or cancelled: the expert's model is unloaded even when its turn failed. | ||
| 84 | +- **The next question reloads the 4B**: a hand-off costs one load of each model, plus a reload of the coordinator. Keeping both loaded is faster, when the memory allows it. | ||
| 85 | +- **llmman also unloads by itself** a model idle for five minutes (`keep_alive`); the hooks free the memory at once, which matters when the next model needs it right away. | ||
| 86 | +- **Turn off `/unload` and `unload_model`** with `unloadModels: false`, as the expert does: an agent that only answers does not need one more tool. The hooks do not depend on that key. | ||
added
demos/unload-models/README.fr.md +86 -0 | new file mode 100644 | ||
| @@ -0,0 +1,86 @@ | ||
| 1 | +# Démo mini-me : décharger des modèles | |
| 2 | + | |
| 3 | +*[English version](README.en.md)* | |
| 4 | + | |
| 5 | +Un coordinateur confie chaque question de programmation à un expert, et les deux modèles se relaient dans la mémoire du serveur sans jamais y être chargés ensemble : un hook `preAsk` décharge le modèle du coordinateur juste avant le lancement de l'expert, et un hook `postAsk` décharge le modèle de l'expert une fois sa réponse donnée, avant que le coordinateur soit rechargé pour la lire. Les modèles sont servis par [llmman](https://github.com/llmmanorg/llmman). | |
| 6 | + | |
| 7 | +## Ce qu'il y a ici | |
| 8 | + | |
| 9 | +| Fichier | Rôle | | |
| 10 | +|---|---| | |
| 11 | +| `agent.yaml` | Le **coordinateur** (Qwen3.5 9B) : `ask_agent` vers l'expert, puis la réponse. Les deux hooks. `/unload` et l'outil `unload_model` fonctionnent aussi ici. `displayHooks: true` affiche l'exécution des hooks. | | |
| 12 | +| `hooks/unload.js` | Le hook (Node.js). `unload.js self` (`preAsk`) : décharge le modèle du coordinateur juste avant que mm lance l'expert. `unload.js asked` (`postAsk`) : décharge le modèle de l'expert une fois sa réponse donnée. | | |
| 13 | +| `agent.expert.yaml` | L'**expert** (Qwen3.5 4B) : répond aux questions de programmation, sans aucun outil (`unloadModels: false`). | | |
| 14 | + | |
| 15 | +## Lancer la démo | |
| 16 | + | |
| 17 | +```bash | |
| 18 | +llmman pull hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M | |
| 19 | +llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | |
| 20 | +cd demos/unload-models | |
| 21 | +mm -tui agent.yaml | |
| 22 | +``` | |
| 23 | + | |
| 24 | +Pose une question de programmation, puis regarde ce que le serveur garde en mémoire : | |
| 25 | + | |
| 26 | +```bash | |
| 27 | +llmman ps # ou : curl -s http://127.0.0.1:17434/api/ps | |
| 28 | +``` | |
| 29 | + | |
| 30 | +Après la réponse, seul le Qwen3.5 9B du coordinateur est chargé. Dans le terminal, le relais s'affiche ainsi : | |
| 31 | + | |
| 32 | +```text | |
| 33 | +🗣 ask_agent: expert (llmman/hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M) — Explain what a closure is in Ja… | |
| 34 | + 🪝 1. preAsk expert · node "$MM_CONFIG_DIR/hooks/unload.js" self → continue | |
| 35 | + 🪝 2. postAsk expert · node "$MM_CONFIG_DIR/hooks/unload.js" asked → continue | |
| 36 | +``` | |
| 37 | + | |
| 38 | +À la main : `/unload` décharge le modèle du coordinateur, `/unload <modèle>` n'importe quel autre modèle du même serveur — dans le terminal, la fenêtre et un éditeur. Demander au coordinateur de libérer la mémoire de l'expert (« Unload the expert's model. », « Libère la mémoire de l'expert. ») lui fait appeler `unload_model` sur le modèle de l'expert (4 sur 4 mesurés) : son nom est écrit dans le prompt, car le modèle ne peut pas deviner comment le serveur nomme un modèle. | |
| 39 | + | |
| 40 | +## Comment ça marche | |
| 41 | + | |
| 42 | +Ce que le serveur garde en mémoire pendant une question (mesuré avec `/api/ps` deux fois par seconde) : | |
| 43 | + | |
| 44 | +| Moment | Chargé | | |
| 45 | +|---|---| | |
| 46 | +| le coordinateur lit la question et appelle `ask_agent` | Qwen3.5 9B | | |
| 47 | +| le hook `preAsk` décharge le coordinateur | — | | |
| 48 | +| l'expert travaille | Qwen3.5 4B seul | | |
| 49 | +| le hook `postAsk` décharge l'expert | — | | |
| 50 | +| le coordinateur lit la réponse (le serveur le recharge) | Qwen3.5 9B seul | | |
| 51 | + | |
| 52 | +**Pourquoi des hooks, et pas le modèle.** Les deux déchargements doivent avoir lieu à un moment où le modèle du coordinateur ne tourne pas : | |
| 53 | + | |
| 54 | +- *Avant l'expert* : le modèle du coordinateur vient de décider d'appeler `ask_agent`, et il n'en a de nouveau besoin que pour lire la réponse. Le modèle ne peut pas se décharger lui-même — `unload_model` sur son propre modèle ne dure que jusqu'à son étape suivante. | |
| 55 | +- *Après l'expert* : si le coordinateur appelait `unload_model` sur le modèle de l'expert, il tournerait déjà de nouveau — rechargé à côté du 4B — au moment de le faire. Les deux modèles seraient chargés ensemble pendant toute cette étape. (C'est ce que faisait la version précédente de cette démo ; voir plus bas.) | |
| 56 | + | |
| 57 | +mm exécute les hooks `preAsk` juste avant de lancer l'agent interrogé et les hooks `postAsk` juste après sa réponse, avant que l'agent qui l'a interrogé la lise. `hooks/unload.js` lit dans son entrée le modèle à décharger — `"model": "llmman/<modèle>"` pour `self`, `"ask": {"agentModel": "llmman/<modèle>"}` pour `asked` — et envoie `keep_alive: 0` au serveur. `matcher: ^expert$` limite les deux hooks à ce relais. `onError: warn` : si un déchargement échoue, le relais continue. | |
| 58 | + | |
| 59 | +Les hooks sont en plus certains : un modèle peut sauter une consigne, un hook s'exécute toujours. | |
| 60 | + | |
| 61 | +## Résultats mesurés (2026-10-18, `mm -p`, llmman) | |
| 62 | + | |
| 63 | +Coordinateur Qwen3.5 9B, expert Qwen3.5 4B, les deux hooks actifs. « Jamais ensemble » : `/api/ps` (interrogé deux fois par seconde) n'a jamais montré les deux modèles à la fois pendant la question : 9B → rien → 4B → rien → 9B. | |
| 64 | + | |
| 65 | +| Question | `ask_agent` | `preAsk` décharge le 9B | `postAsk` décharge le 4B | jamais ensemble | Durée | | |
| 66 | +|---|---|---|---|---|---| | |
| 67 | +| How do I reverse a string in Go? | ✓ | ✓ | ✓ | ✓ | 62 s | | |
| 68 | +| What is a closure in JavaScript? | ✓ | ✓ | ✓ | ✓ | 65 s | | |
| 69 | +| How do I read a file line by line in Python? | ✓ | ✓ | ✓ | ✓ | 49 s | | |
| 70 | +| Explain Rust ownership briefly. | ✓ | ✓ | ✓ | ✓ | 82 s | | |
| 71 | +| Comment trier un tableau en Java ? | ✓ | ✓ | ✓ | ✓ | 119 s | | |
| 72 | +| How do I make an HTTP GET request in Go? | ✓ | ✓ | ✓ | ✓ | 106 s | | |
| 73 | +| What does the yield keyword do in Python? (×3) | ✓ ✓ ✓ | ✓ ✓ ✓ | ✓ ✓ ✓ | ✓ ✓ ✓ | 57 s, 60 s, 85 s | | |
| 74 | + | |
| 75 | +9 sur 9. La question en français reçoit une réponse en français. Une salutation (« Hello, who are you? ») reçoit une réponse en 2 s sans aucun outil. Les durées comprennent les chargements : chaque question charge le 9B deux fois et le 4B une fois, et elles varient beaucoup d'une exécution à l'autre. | |
| 76 | + | |
| 77 | +**Le prompt.** Sans l'étape `unload_model`, un prompt court (« 1. Call ask_agent… 2. Then give the expert's answer ») a laissé le coordinateur répondre seul à la question sur `yield`, sans l'expert (1 fois sur 7). Préciser que `ask_agent` est TOUJOURS la première action, « even for a question you know the answer to », a donné le tableau ci-dessus. | |
| 78 | + | |
| 79 | +**Versions précédentes.** Quand le coordinateur libérait l'expert avec `unload_model` (sans hook `postAsk`), le 9B et le 4B étaient chargés ensemble pendant ~18 s par question, le temps que le coordinateur lise la réponse — 7 questions sur 7 sans autre défaut. Avant cela, avec un coordinateur MiniCPM5 2B et un expert Qwen3.5 9B, le coordinateur a sauté `unload_model` une fois sur cinq. | |
| 80 | + | |
| 81 | +## Ce qu'il faut savoir | |
| 82 | + | |
| 83 | +- **`postAsk` s'exécute après chaque relais vers l'expert**, qu'il ait répondu, échoué ou été annulé : le modèle de l'expert est déchargé même quand son tour a échoué. | |
| 84 | +- **La question suivante recharge le 4B** : un relais coûte un chargement de chaque modèle, plus un rechargement du coordinateur. Garder les deux chargés est plus rapide, quand la mémoire le permet. | |
| 85 | +- **llmman décharge aussi tout seul** un modèle inactif depuis cinq minutes (`keep_alive`) ; les hooks libèrent la mémoire tout de suite, ce qui compte quand le modèle suivant en a besoin aussitôt. | |
| 86 | +- **Désactive `/unload` et `unload_model`** avec `unloadModels: false`, comme l'expert : un agent qui ne fait que répondre n'a pas besoin d'un outil de plus. Les hooks ne dépendent pas de cette clé. | |
| new file mode 100644 | |||
| @@ -0,0 +1,86 @@ | |||
| 1 | +# Démo mini-me : décharger des modèles | ||
| 2 | + | ||
| 3 | +*[English version](README.en.md)* | ||
| 4 | + | ||
| 5 | +Un coordinateur confie chaque question de programmation à un expert, et les deux modèles se relaient dans la mémoire du serveur sans jamais y être chargés ensemble : un hook `preAsk` décharge le modèle du coordinateur juste avant le lancement de l'expert, et un hook `postAsk` décharge le modèle de l'expert une fois sa réponse donnée, avant que le coordinateur soit rechargé pour la lire. Les modèles sont servis par [llmman](https://github.com/llmmanorg/llmman). | ||
| 6 | + | ||
| 7 | +## Ce qu'il y a ici | ||
| 8 | + | ||
| 9 | +| Fichier | Rôle | | ||
| 10 | +|---|---| | ||
| 11 | +| `agent.yaml` | Le **coordinateur** (Qwen3.5 9B) : `ask_agent` vers l'expert, puis la réponse. Les deux hooks. `/unload` et l'outil `unload_model` fonctionnent aussi ici. `displayHooks: true` affiche l'exécution des hooks. | | ||
| 12 | +| `hooks/unload.js` | Le hook (Node.js). `unload.js self` (`preAsk`) : décharge le modèle du coordinateur juste avant que mm lance l'expert. `unload.js asked` (`postAsk`) : décharge le modèle de l'expert une fois sa réponse donnée. | | ||
| 13 | +| `agent.expert.yaml` | L'**expert** (Qwen3.5 4B) : répond aux questions de programmation, sans aucun outil (`unloadModels: false`). | | ||
| 14 | + | ||
| 15 | +## Lancer la démo | ||
| 16 | + | ||
| 17 | +```bash | ||
| 18 | +llmman pull hf.co/unsloth/Qwen3.5-9B-GGUF:Q4_K_M | ||
| 19 | +llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | ||
| 20 | +cd demos/unload-models | ||
| 21 | +mm -tui agent.yaml | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +Pose une question de programmation, puis regarde ce que le serveur garde en mémoire : | ||
| 25 | + | ||
| 26 | +```bash | ||
| 27 | +llmman ps # ou : curl -s http://127.0.0.1:17434/api/ps | ||
| 28 | +``` | ||
| 29 | + | ||
| 30 | +Après la réponse, seul le Qwen3.5 9B du coordinateur est chargé. Dans le terminal, le relais s'affiche ainsi : | ||
| 31 | + | ||
| 32 | +```text | ||
| 33 | +🗣 ask_agent: expert (llmman/hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M) — Explain what a closure is in Ja… | ||
| 34 | + 🪝 1. preAsk expert · node "$MM_CONFIG_DIR/hooks/unload.js" self → continue | ||
| 35 | + 🪝 2. postAsk expert · node "$MM_CONFIG_DIR/hooks/unload.js" asked → continue | ||
| 36 | +``` | ||
| 37 | + | ||
| 38 | +À la main : `/unload` décharge le modèle du coordinateur, `/unload <modèle>` n'importe quel autre modèle du même serveur — dans le terminal, la fenêtre et un éditeur. Demander au coordinateur de libérer la mémoire de l'expert (« Unload the expert's model. », « Libère la mémoire de l'expert. ») lui fait appeler `unload_model` sur le modèle de l'expert (4 sur 4 mesurés) : son nom est écrit dans le prompt, car le modèle ne peut pas deviner comment le serveur nomme un modèle. | ||
| 39 | + | ||
| 40 | +## Comment ça marche | ||
| 41 | + | ||
| 42 | +Ce que le serveur garde en mémoire pendant une question (mesuré avec `/api/ps` deux fois par seconde) : | ||
| 43 | + | ||
| 44 | +| Moment | Chargé | | ||
| 45 | +|---|---| | ||
| 46 | +| le coordinateur lit la question et appelle `ask_agent` | Qwen3.5 9B | | ||
| 47 | +| le hook `preAsk` décharge le coordinateur | — | | ||
| 48 | +| l'expert travaille | Qwen3.5 4B seul | | ||
| 49 | +| le hook `postAsk` décharge l'expert | — | | ||
| 50 | +| le coordinateur lit la réponse (le serveur le recharge) | Qwen3.5 9B seul | | ||
| 51 | + | ||
| 52 | +**Pourquoi des hooks, et pas le modèle.** Les deux déchargements doivent avoir lieu à un moment où le modèle du coordinateur ne tourne pas : | ||
| 53 | + | ||
| 54 | +- *Avant l'expert* : le modèle du coordinateur vient de décider d'appeler `ask_agent`, et il n'en a de nouveau besoin que pour lire la réponse. Le modèle ne peut pas se décharger lui-même — `unload_model` sur son propre modèle ne dure que jusqu'à son étape suivante. | ||
| 55 | +- *Après l'expert* : si le coordinateur appelait `unload_model` sur le modèle de l'expert, il tournerait déjà de nouveau — rechargé à côté du 4B — au moment de le faire. Les deux modèles seraient chargés ensemble pendant toute cette étape. (C'est ce que faisait la version précédente de cette démo ; voir plus bas.) | ||
| 56 | + | ||
| 57 | +mm exécute les hooks `preAsk` juste avant de lancer l'agent interrogé et les hooks `postAsk` juste après sa réponse, avant que l'agent qui l'a interrogé la lise. `hooks/unload.js` lit dans son entrée le modèle à décharger — `"model": "llmman/<modèle>"` pour `self`, `"ask": {"agentModel": "llmman/<modèle>"}` pour `asked` — et envoie `keep_alive: 0` au serveur. `matcher: ^expert$` limite les deux hooks à ce relais. `onError: warn` : si un déchargement échoue, le relais continue. | ||
| 58 | + | ||
| 59 | +Les hooks sont en plus certains : un modèle peut sauter une consigne, un hook s'exécute toujours. | ||
| 60 | + | ||
| 61 | +## Résultats mesurés (2026-10-18, `mm -p`, llmman) | ||
| 62 | + | ||
| 63 | +Coordinateur Qwen3.5 9B, expert Qwen3.5 4B, les deux hooks actifs. « Jamais ensemble » : `/api/ps` (interrogé deux fois par seconde) n'a jamais montré les deux modèles à la fois pendant la question : 9B → rien → 4B → rien → 9B. | ||
| 64 | + | ||
| 65 | +| Question | `ask_agent` | `preAsk` décharge le 9B | `postAsk` décharge le 4B | jamais ensemble | Durée | | ||
| 66 | +|---|---|---|---|---|---| | ||
| 67 | +| How do I reverse a string in Go? | ✓ | ✓ | ✓ | ✓ | 62 s | | ||
| 68 | +| What is a closure in JavaScript? | ✓ | ✓ | ✓ | ✓ | 65 s | | ||
| 69 | +| How do I read a file line by line in Python? | ✓ | ✓ | ✓ | ✓ | 49 s | | ||
| 70 | +| Explain Rust ownership briefly. | ✓ | ✓ | ✓ | ✓ | 82 s | | ||
| 71 | +| Comment trier un tableau en Java ? | ✓ | ✓ | ✓ | ✓ | 119 s | | ||
| 72 | +| How do I make an HTTP GET request in Go? | ✓ | ✓ | ✓ | ✓ | 106 s | | ||
| 73 | +| What does the yield keyword do in Python? (×3) | ✓ ✓ ✓ | ✓ ✓ ✓ | ✓ ✓ ✓ | ✓ ✓ ✓ | 57 s, 60 s, 85 s | | ||
| 74 | + | ||
| 75 | +9 sur 9. La question en français reçoit une réponse en français. Une salutation (« Hello, who are you? ») reçoit une réponse en 2 s sans aucun outil. Les durées comprennent les chargements : chaque question charge le 9B deux fois et le 4B une fois, et elles varient beaucoup d'une exécution à l'autre. | ||
| 76 | + | ||
| 77 | +**Le prompt.** Sans l'étape `unload_model`, un prompt court (« 1. Call ask_agent… 2. Then give the expert's answer ») a laissé le coordinateur répondre seul à la question sur `yield`, sans l'expert (1 fois sur 7). Préciser que `ask_agent` est TOUJOURS la première action, « even for a question you know the answer to », a donné le tableau ci-dessus. | ||
| 78 | + | ||
| 79 | +**Versions précédentes.** Quand le coordinateur libérait l'expert avec `unload_model` (sans hook `postAsk`), le 9B et le 4B étaient chargés ensemble pendant ~18 s par question, le temps que le coordinateur lise la réponse — 7 questions sur 7 sans autre défaut. Avant cela, avec un coordinateur MiniCPM5 2B et un expert Qwen3.5 9B, le coordinateur a sauté `unload_model` une fois sur cinq. | ||
| 80 | + | ||
| 81 | +## Ce qu'il faut savoir | ||
| 82 | + | ||
| 83 | +- **`postAsk` s'exécute après chaque relais vers l'expert**, qu'il ait répondu, échoué ou été annulé : le modèle de l'expert est déchargé même quand son tour a échoué. | ||
| 84 | +- **La question suivante recharge le 4B** : un relais coûte un chargement de chaque modèle, plus un rechargement du coordinateur. Garder les deux chargés est plus rapide, quand la mémoire le permet. | ||
| 85 | +- **llmman décharge aussi tout seul** un modèle inactif depuis cinq minutes (`keep_alive`) ; les hooks libèrent la mémoire tout de suite, ce qui compte quand le modèle suivant en a besoin aussitôt. | ||
| 86 | +- **Désactive `/unload` et `unload_model`** avec `unloadModels: false`, comme l'expert : un agent qui ne fait que répondre n'a pas besoin d'un outil de plus. Les hooks ne dépendent pas de cette clé. | ||
added
demos/unload-models/agent.expert.yaml +37 -0 | new file mode 100644 | ||
| @@ -0,0 +1,37 @@ | ||
| 1 | +# The expert of demos/unload-models: answers programming questions. Asked by | |
| 2 | +# agent.yaml through ask_agent; its model is unloaded after each answer. | |
| 3 | + | |
| 4 | +provider: llmman | |
| 5 | +model: hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | |
| 6 | +baseUrl: http://127.0.0.1:17434/v1 | |
| 7 | +fallback: http://host.docker.internal:17434/v1 | |
| 8 | + | |
| 9 | +sampling: | |
| 10 | + temperature: 0.2 | |
| 11 | + max_tokens: 2048 | |
| 12 | + chat_template_kwargs: | |
| 13 | + enable_thinking: false | |
| 14 | + | |
| 15 | +watchdogTimeout: 5m | |
| 16 | +firstTokenTimeout: 3m | |
| 17 | +maxTurns: 2 | |
| 18 | + | |
| 19 | +# The expert only answers: no tools at all, unload_model included. | |
| 20 | +unloadModels: false | |
| 21 | +bashTool: false | |
| 22 | +editTools: false | |
| 23 | +planTool: false | |
| 24 | +fetchContentTool: false | |
| 25 | +askUserTool: false | |
| 26 | +skillsDir: no-skills | |
| 27 | + | |
| 28 | +agentsMd: | |
| 29 | + enabled: false | |
| 30 | + | |
| 31 | +acp: | |
| 32 | + name: expert | |
| 33 | + title: Programming expert | |
| 34 | + | |
| 35 | +system: | | |
| 36 | + You are a programming expert. Answer the question clearly and briefly: | |
| 37 | + a short explanation, then one short code example when it helps. | |
| new file mode 100644 | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | +# The expert of demos/unload-models: answers programming questions. Asked by | ||
| 2 | +# agent.yaml through ask_agent; its model is unloaded after each answer. | ||
| 3 | + | ||
| 4 | +provider: llmman | ||
| 5 | +model: hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | ||
| 6 | +baseUrl: http://127.0.0.1:17434/v1 | ||
| 7 | +fallback: http://host.docker.internal:17434/v1 | ||
| 8 | + | ||
| 9 | +sampling: | ||
| 10 | + temperature: 0.2 | ||
| 11 | + max_tokens: 2048 | ||
| 12 | + chat_template_kwargs: | ||
| 13 | + enable_thinking: false | ||
| 14 | + | ||
| 15 | +watchdogTimeout: 5m | ||
| 16 | +firstTokenTimeout: 3m | ||
| 17 | +maxTurns: 2 | ||
| 18 | + | ||
| 19 | +# The expert only answers: no tools at all, unload_model included. | ||
| 20 | +unloadModels: false | ||
| 21 | +bashTool: false | ||
| 22 | +editTools: false | ||
| 23 | +planTool: false | ||
| 24 | +fetchContentTool: false | ||
| 25 | +askUserTool: false | ||
| 26 | +skillsDir: no-skills | ||
| 27 | + | ||
| 28 | +agentsMd: | ||
| 29 | + enabled: false | ||
| 30 | + | ||
| 31 | +acp: | ||
| 32 | + name: expert | ||
| 33 | + title: Programming expert | ||
| 34 | + | ||
| 35 | +system: | | ||
| 36 | + You are a programming expert. Answer the question clearly and briefly: | ||
| 37 | + a short explanation, then one short code example when it helps. | ||
added
demos/unload-models/agent.yaml +108 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
demos/unload-models/hooks/unload.js +58 -0 | new file mode 100644 | ||
| @@ -0,0 +1,58 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// preAsk / postAsk hook: unloads one model so that two models are never in | |
| 3 | +// memory together during a hand-off. | |
| 4 | +// | |
| 5 | +// node unload.js self preAsk: the ASKING agent's own model, right before | |
| 6 | +// mm starts the asked agent. Only mm can do it at | |
| 7 | +// that moment: the model has just decided to call | |
| 8 | +// ask_agent, and it is needed again only to read the | |
| 9 | +// answer — the server reloads it then by itself. The | |
| 10 | +// model cannot do it (unload_model on its own model | |
| 11 | +// lasts until its next step). | |
| 12 | +// node unload.js asked postAsk: the ASKED agent's model, once it has | |
| 13 | +// answered and before the asking agent reads the | |
| 14 | +// answer — so the asker's model reloads alone. | |
| 15 | +// | |
| 16 | +// Input: {"model": "llmman/<model>", "ask": {"agentModel": "llmman/<model>"}, | |
| 17 | +// ...}. Server: $LLMMAN_URL, else llmman's default address, else the one a | |
| 18 | +// Docker sandbox reaches the host by. Prints nothing: the hand-off goes on | |
| 19 | +// whatever happens (onError: warn in agent.yaml), an unload is a saving, not | |
| 20 | +// a check. | |
| 21 | + | |
| 22 | +const fs = require("fs"); | |
| 23 | + | |
| 24 | +const input = JSON.parse(fs.readFileSync(0, "utf8")); | |
| 25 | +const which = process.argv[2]; | |
| 26 | +const ref = which === "asked" ? input.ask && input.ask.agentModel : input.model; | |
| 27 | +if (!ref) { | |
| 28 | + console.error(`unload.js ${which}: no model in the input`); | |
| 29 | + process.exit(1); | |
| 30 | +} | |
| 31 | +const model = ref.replace(/^[^/]+\//, ""); // drop the "llmman/" provider prefix | |
| 32 | + | |
| 33 | +const servers = process.env.LLMMAN_URL | |
| 34 | + ? [process.env.LLMMAN_URL] | |
| 35 | + : ["http://127.0.0.1:17434", "http://host.docker.internal:17434"]; | |
| 36 | + | |
| 37 | +async function unload() { | |
| 38 | + for (const server of servers) { | |
| 39 | + try { | |
| 40 | + const resp = await fetch(server + "/api/generate", { | |
| 41 | + method: "POST", | |
| 42 | + headers: { "Content-Type": "application/json" }, | |
| 43 | + body: JSON.stringify({ model, keep_alive: 0 }), | |
| 44 | + signal: AbortSignal.timeout(20_000), | |
| 45 | + }); | |
| 46 | + const reply = await resp.json(); | |
| 47 | + if (reply.done_reason === "unload") return; | |
| 48 | + throw new Error(`${server}: not confirmed: ${JSON.stringify(reply)}`); | |
| 49 | + } catch (err) { | |
| 50 | + if (server === servers[servers.length - 1]) throw err; | |
| 51 | + } | |
| 52 | + } | |
| 53 | +} | |
| 54 | + | |
| 55 | +unload().catch((err) => { | |
| 56 | + console.error(`could not unload ${model}: ${err.message}`); | |
| 57 | + process.exit(1); | |
| 58 | +}); | |
| new file mode 100644 | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | +#!/usr/bin/env node | ||
| 2 | +// preAsk / postAsk hook: unloads one model so that two models are never in | ||
| 3 | +// memory together during a hand-off. | ||
| 4 | +// | ||
| 5 | +// node unload.js self preAsk: the ASKING agent's own model, right before | ||
| 6 | +// mm starts the asked agent. Only mm can do it at | ||
| 7 | +// that moment: the model has just decided to call | ||
| 8 | +// ask_agent, and it is needed again only to read the | ||
| 9 | +// answer — the server reloads it then by itself. The | ||
| 10 | +// model cannot do it (unload_model on its own model | ||
| 11 | +// lasts until its next step). | ||
| 12 | +// node unload.js asked postAsk: the ASKED agent's model, once it has | ||
| 13 | +// answered and before the asking agent reads the | ||
| 14 | +// answer — so the asker's model reloads alone. | ||
| 15 | +// | ||
| 16 | +// Input: {"model": "llmman/<model>", "ask": {"agentModel": "llmman/<model>"}, | ||
| 17 | +// ...}. Server: $LLMMAN_URL, else llmman's default address, else the one a | ||
| 18 | +// Docker sandbox reaches the host by. Prints nothing: the hand-off goes on | ||
| 19 | +// whatever happens (onError: warn in agent.yaml), an unload is a saving, not | ||
| 20 | +// a check. | ||
| 21 | + | ||
| 22 | +const fs = require("fs"); | ||
| 23 | + | ||
| 24 | +const input = JSON.parse(fs.readFileSync(0, "utf8")); | ||
| 25 | +const which = process.argv[2]; | ||
| 26 | +const ref = which === "asked" ? input.ask && input.ask.agentModel : input.model; | ||
| 27 | +if (!ref) { | ||
| 28 | + console.error(`unload.js ${which}: no model in the input`); | ||
| 29 | + process.exit(1); | ||
| 30 | +} | ||
| 31 | +const model = ref.replace(/^[^/]+\//, ""); // drop the "llmman/" provider prefix | ||
| 32 | + | ||
| 33 | +const servers = process.env.LLMMAN_URL | ||
| 34 | + ? [process.env.LLMMAN_URL] | ||
| 35 | + : ["http://127.0.0.1:17434", "http://host.docker.internal:17434"]; | ||
| 36 | + | ||
| 37 | +async function unload() { | ||
| 38 | + for (const server of servers) { | ||
| 39 | + try { | ||
| 40 | + const resp = await fetch(server + "/api/generate", { | ||
| 41 | + method: "POST", | ||
| 42 | + headers: { "Content-Type": "application/json" }, | ||
| 43 | + body: JSON.stringify({ model, keep_alive: 0 }), | ||
| 44 | + signal: AbortSignal.timeout(20_000), | ||
| 45 | + }); | ||
| 46 | + const reply = await resp.json(); | ||
| 47 | + if (reply.done_reason === "unload") return; | ||
| 48 | + throw new Error(`${server}: not confirmed: ${JSON.stringify(reply)}`); | ||
| 49 | + } catch (err) { | ||
| 50 | + if (server === servers[servers.length - 1]) throw err; | ||
| 51 | + } | ||
| 52 | + } | ||
| 53 | +} | ||
| 54 | + | ||
| 55 | +unload().catch((err) => { | ||
| 56 | + console.error(`could not unload ${model}: ${err.message}`); | ||
| 57 | + process.exit(1); | ||
| 58 | +}); | ||
modified
docs/en/README.md +2 -1 Large diff collapsed to keep the page fast — load it when you need it.
added
docs/en/how-to/unload-a-model.md +58 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/en/reference/configuration.md +2 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/en/reference/slash-commands.md +3 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/en/reference/tools.md +20 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/README.md +2 -1 Large diff collapsed to keep the page fast — load it when you need it.
added
docs/fr/how-to/unload-a-model.md +58 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/reference/configuration.md +2 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/reference/slash-commands.md +3 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/reference/tools.md +20 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/acp/acp.go +12 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/acp/unload_test.go +55 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/agent/agent.go +2 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/command/command.go +7 -3 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/command/unload.go +23 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/command/unload_test.go +30 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/config/config.go +8 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/config/config_test.go +20 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/config/tools.go +1 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/engine/openai_compat.go +6 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/engine/unload.go +196 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/engine/unload_test.go +139 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/tools/unload.go +69 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/tools/unload_test.go +62 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/commands.go +12 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/draw.go +13 -3 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/pickers_test.go +18 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/turn.go +3 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/turn_test.go +25 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
main.go +5 -0 Large diff collapsed to keep the page fast — load it when you need it.