bots-garden/mini-mepublic Fork
⑂ main
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Unload model(s) #34

Merged@k33g wants to merge feature/unload-model into main
Merged as 07f685d44f12.
44 files changed
modified .tickets/issues/0092-how-to-unload-a-model.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 92
33 title: How to unload a model
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-17T14:24:18.694Z
9-updatedAt: 2026-10-18T16:00:00.000Z
9+updatedAt: 2026-10-18T19:30:22.673Z
1010 labels:
1111 - priority::urgent
1212 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: 922 id: 92
3 title: How to unload a model3 title: How to unload a model
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-17T14:24:18.694Z8 createdAt: 2026-10-17T14:24:18.694Z
9-updatedAt: 2026-10-18T16:00:00.000Z9+updatedAt: 2026-10-18T19:30:22.673Z
10 labels:10 labels:
11 - priority::urgent11 - priority::urgent
12 milestone: v0.7.012 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
120120 - **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/`.
121121 - **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).
122122 - **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/`.
123124 - **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).
124125
125126 ## Demos
@@ -152,6 +153,7 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h
152153 | `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 |
153154 | `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 |
154155 | `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 |
155157 | `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 |
156158
157159 ## 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 ## Demos126 ## 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 more159 ## 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
3030
3131 ## How it works
3232
33+![The demos/guards flow: the question goes through a preGeneration hook that asks seven guard agents, then to the assistant](guards-flow.svg)
34+
35+Editable source: `guards-flow.drawio` (draw.io); `guards-flow.svg` is the same picture.
36+
3337 ```text
3438 user's question
3539 │
@@ -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 works31 ## How it works
32 32
33+![The demos/guards flow: the question goes through a preGeneration hook that asks seven guard agents, then to the assistant](guards-flow.svg)
34+
35+Editable source: `guards-flow.drawio` (draw.io); `guards-flow.svg` is the same picture.
36+
33 ```text37 ```text
34 user's question38 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
3030
3131 ## Comment ça marche
3232
33+![Le flux de demos/guards : la question passe par un hook preGeneration qui interroge sept agents de garde, puis va à l'assistant](guards-flow.svg)
34+
35+Source modifiable : `guards-flow.drawio` (draw.io) ; `guards-flow.svg` est la même image.
36+
3337 ```text
3438 question de l'utilisateur
3539 │
@@ -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 marche31 ## Comment ça marche
32 32
33+![Le flux de demos/guards : la question passe par un hook preGeneration qui interroge sept agents de garde, puis va à l'assistant](guards-flow.svg)
34+
35+Source modifiable : `guards-flow.drawio` (draw.io) ; `guards-flow.svg` est la même image.
36+
33 ```text37 ```text
34 question de l'utilisateur38 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="🧑&lt;br&gt;&lt;b&gt;User&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:11px&quot;&gt;asks a question&lt;br&gt;terminal · -tui · editor&lt;/font&gt;" 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="🔍&lt;br&gt;&lt;b&gt;preGeneration hook&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:11px&quot;&gt;hooks/guards.js&lt;br&gt;no verdict → stop (fail closed)&lt;/font&gt;" 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="🤖&lt;br&gt;&lt;b&gt;Assistant&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:11px&quot;&gt;Gemma 4 E4B · no tools&lt;br&gt;all No → answers · a Yes → refuses&lt;/font&gt;" 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="&lt;b&gt;harm&lt;/b&gt;" 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="&lt;b&gt;jailbreak&lt;/b&gt;" 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="&lt;b&gt;violence&lt;/b&gt;" 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="&lt;b&gt;unethical_behavior&lt;/b&gt;" 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="&lt;b&gt;social_bias&lt;/b&gt;" 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="&lt;b&gt;profanity&lt;/b&gt;" 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="&lt;b&gt;sexual_content&lt;/b&gt;" 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/&amp;lt;risk&amp;gt;.yaml -p=&quot;&amp;lt;question&amp;gt;&quot;&lt;br&gt;system: &amp;lt;risk name&amp;gt; · a fresh conversation, never the history&lt;br&gt;&lt;i&gt;&lt;font color=&quot;#78909C&quot; style=&quot;font-size:11px&quot;&gt;answers &quot;Yes&quot; or &quot;No&quot; + &amp;lt;confidence&amp;gt;&lt;/font&gt;&lt;/i&gt;" 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&lt;br&gt;&lt;font style=&quot;font-size:10px;font-weight:normal&quot; color=&quot;#607D8B&quot;&gt;[context added by a hook]&lt;/font&gt;" 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&lt;br&gt;&lt;font style=&quot;font-size:10px;font-weight:normal&quot; color=&quot;#607D8B&quot;&gt;← Yes / No per risk&lt;/font&gt;" 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="🧑&lt;br&gt;&lt;b&gt;User&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:11px&quot;&gt;asks a question&lt;br&gt;terminal · -tui · editor&lt;/font&gt;" 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="🔍&lt;br&gt;&lt;b&gt;preGeneration hook&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:11px&quot;&gt;hooks/guards.js&lt;br&gt;no verdict → stop (fail closed)&lt;/font&gt;" 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="🤖&lt;br&gt;&lt;b&gt;Assistant&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size:11px&quot;&gt;Gemma 4 E4B · no tools&lt;br&gt;all No → answers · a Yes → refuses&lt;/font&gt;" 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="&lt;b&gt;harm&lt;/b&gt;" 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="&lt;b&gt;jailbreak&lt;/b&gt;" 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="&lt;b&gt;violence&lt;/b&gt;" 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="&lt;b&gt;unethical_behavior&lt;/b&gt;" 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="&lt;b&gt;social_bias&lt;/b&gt;" 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="&lt;b&gt;profanity&lt;/b&gt;" 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="&lt;b&gt;sexual_content&lt;/b&gt;" 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/&amp;lt;risk&amp;gt;.yaml -p=&quot;&amp;lt;question&amp;gt;&quot;&lt;br&gt;system: &amp;lt;risk name&amp;gt; · a fresh conversation, never the history&lt;br&gt;&lt;i&gt;&lt;font color=&quot;#78909C&quot; style=&quot;font-size:11px&quot;&gt;answers &quot;Yes&quot; or &quot;No&quot; + &amp;lt;confidence&amp;gt;&lt;/font&gt;&lt;/i&gt;" 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&lt;br&gt;&lt;font style=&quot;font-size:10px;font-weight:normal&quot; color=&quot;#607D8B&quot;&gt;[context added by a hook]&lt;/font&gt;" 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&lt;br&gt;&lt;font style=&quot;font-size:10px;font-weight:normal&quot; color=&quot;#607D8B&quot;&gt;← Yes / No per risk&lt;/font&gt;" 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 differBinary 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/&lt;risk&gt;.yaml -p="&lt;question&gt;"</text>
115+ <text x="600" y="586" text-anchor="middle" font-size="12" fill="#455A64">system: &lt;risk name&gt; · 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" + &lt;confidence&gt;</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/&lt;risk&gt;.yaml -p="&lt;question&gt;"</text>
115+ <text x="600" y="586" text-anchor="middle" font-size="12" fill="#455A64">system: &lt;risk name&gt; · 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" + &lt;confidence&gt;</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.