bots-garden/mini-mepublic Fork
⑂ 9d1939f
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

Merge feature/ask-cli into main

rickub committed 2026-10-03T08:18:28Z Browse files
9d1939f parents: 8654e3f 382fecb
modified .tickets/issues/0052-buitin-acp-tool-to-query-other-agent-like-mini-m.yaml +2 -1
@@ -6,7 +6,7 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-29T05:18:13.897Z
9-updatedAt: 2026-09-29T06:25:58.324Z
9+updatedAt: 2026-10-02T21:15:00.000Z
1010 labels:
1111 - priority::high
1212 - kind::feature
@@ -15,6 +15,7 @@ labels:
1515 milestone: v0.3.0
1616 related:
1717 - 33
18+ - 66
1819 body: ""
1920 tasks: []
2021 comments: []
@@ -6,7 +6,7 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-29T05:18:13.897Z8 createdAt: 2026-09-29T05:18:13.897Z
9-updatedAt: 2026-09-29T06:25:58.324Z9+updatedAt: 2026-10-02T21:15:00.000Z
10 labels:10 labels:
11 - priority::high11 - priority::high
12 - kind::feature12 - kind::feature
@@ -15,6 +15,7 @@ labels:
15 milestone: v0.3.015 milestone: v0.3.0
16 related:16 related:
17 - 3317 - 33
18+ - 66
18 body: ""19 body: ""
19 tasks: []20 tasks: []
20 comments: []21 comments: []
modified .tickets/issues/0066-talk-to-another-mm.yaml +2 -1
@@ -6,10 +6,11 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-02T18:48:36.275Z
9-updatedAt: 2026-10-02T20:30:00.000Z
9+updatedAt: 2026-10-02T21:15:00.000Z
1010 labels:
1111 - kind::feature
1212 related:
13+ - 52
1314 - 57
1415 - 59
1516 body: |
@@ -6,10 +6,11 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-02T18:48:36.275Z8 createdAt: 2026-10-02T18:48:36.275Z
9-updatedAt: 2026-10-02T20:30:00.000Z9+updatedAt: 2026-10-02T21:15:00.000Z
10 labels:10 labels:
11 - kind::feature11 - kind::feature
12 related:12 related:
13+ - 52
13 - 5714 - 57
14 - 5915 - 59
15 body: |16 body: |
renamed .tickets/issues/0071-mini-me-as-a-sdk.yaml +2 -2
similarity index 84%
rename from .tickets/issues/0066-mini-me-as-a-sdk.yaml
rename to .tickets/issues/0071-mini-me-as-a-sdk.yaml
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2-id: 66
2+id: 71
33 title: Mini Me as a SDK
44 state: open
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-10-02T05:32:33.878Z
9-updatedAt: 2026-10-02T05:32:33.878Z
9+updatedAt: 2026-10-02T21:15:00.000Z
1010 labels: []
1111 body: ""
1212 tasks: []
similarity index 84%
rename from .tickets/issues/0066-mini-me-as-a-sdk.yaml
rename to .tickets/issues/0071-mini-me-as-a-sdk.yaml
@@ -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: 662+id: 71
3 title: Mini Me as a SDK3 title: Mini Me as a SDK
4 state: open4 state: open
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-10-02T05:32:33.878Z8 createdAt: 2026-10-02T05:32:33.878Z
9-updatedAt: 2026-10-02T05:32:33.878Z9+updatedAt: 2026-10-02T21:15:00.000Z
10 labels: []10 labels: []
11 body: ""11 body: ""
12 tasks: []12 tasks: []
added .tickets/issues/0072-use-a-decision-model-as-a-local-decider.yaml +242 -0
new file mode 100644
@@ -0,0 +1,242 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 72
3+title: Use a decision model (decision-4b) as a local decider
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-02T21:40:00.000Z
9+updatedAt: 2026-10-02T21:40:00.000Z
10+labels:
11+ - kind::feature
12+related:
13+ - 15
14+ - 59
15+ - 66
16+body: |
17+ Use a DECISION model (e.g. https://huggingface.co/evalengine/decision-4b)
18+ beside the main model, as a fast local "decider" mm consults at precise
19+ moments. Not as mm's `model:`: such a model generates no text and calls no
20+ tools.
21+
22+ ## What the model is (from its card, 2026-10-02)
23+
24+ - LoRA (65 MB) on Qwen/Qwen3.5-4B; ready GGUF: evalengine/decision-4b-gguf
25+ (Q4_K_M 2.7 GB, Q8_0, F16). Smaller sibling: evalengine/decision-0.8b(-gguf).
26+ - In: a `state`, a `question`, 2..24 `options` (letter label, key,
27+ description). Out: one option + a probability per option, read from the
28+ next-token logits. One forward pass, nothing generated.
29+ - Limits: 2048-token context, English only, weaker on multi-rule policies,
30+ probabilities not calibrated, "not for unattended high-stakes decisions".
31+
32+ ## Transport
33+
34+ 1. Preferred: llmman `POST /v1/systemone` (built for this; llama.cpp has no
35+ such endpoint). A `state` + up to 64 typed questions: `choice` (answer,
36+ confidence, probabilities in the order sent, first option = A), `noul`
37+ (probability of yes), `score` (probability-weighted level + legend).
38+ `x_label_mass` = probability put on the labels at all (low = the model
39+ wanted to say something else). Runs with enable_thinking false; loads the
40+ model in its own backend (`<model>#systemone` in `llmman ps`).
41+ 2. Fallback: llama-server / DMR `/v1/chat/completions` with `max_tokens: 1`,
42+ `logprobs: true`, `top_logprobs: N`, the Qwen chat template and the card's
43+ system prompt; renormalise over the option letters found in the top N
44+ (a letter outside the top N gets 0 — less exact than systemone).
45+ Ollama: logprobs support to be checked.
46+
47+ ## Config (proposal)
48+
49+ decisionModel:
50+ provider: llmman # systemone needs llmman
51+ baseUrl: http://127.0.0.1:17434 # NOT /v1: /v1/systemone is added
52+ model: hf.co/evalengine/decision-4b-gguf:Q4_K_M
53+ api: systemone # or: logprobs (llama-server, DMR)
54+ minConfidence: 0.6 # below: do nothing, as if absent
55+ skillRouting: true # use 1
56+ commandTriage: false # use 2
57+ askRouting: true # use 3
58+
59+ ## Use 1 — choose the skill before the turn (start here)
60+
61+ Today the main model reads the read_skill catalogue and may or may not load
62+ the right procedure (measured by the MM_EVAL routing eval, internal/eval).
63+ With a decider: before the turn, ask which skill applies; above
64+ minConfidence, append the existing `$skill` notation to the question, so the
65+ model still makes the read_skill call itself (loop detector, permission
66+ gate and recap still see it).
67+
68+ Example request:
69+
70+ POST /v1/systemone
71+ {
72+ "model": "hf.co/evalengine/decision-4b-gguf:Q4_K_M",
73+ "state": "User request: start the dev server and tell me when it answers on port 8080",
74+ "questions": {
75+ "skill": {
76+ "type": "choice",
77+ "instructions": "Which procedure should the coding agent follow for this request?",
78+ "criteria": {
79+ "bg-jobs": "Start, watch or stop a long-running command in the background",
80+ "lsp-navigation": "Find a definition, references or symbols in the code",
81+ "plan-manager": "Plan and track a multi-step task",
82+ "what-time-is-it": "Tell the current date or time",
83+ "greetings": "Greet someone",
84+ "none": "No listed procedure applies"
85+ }
86+ }
87+ }
88+ }
89+
90+ Expected shape of the answer (values illustrative, not measured):
91+
92+ {"answers": {"skill": {"answer": "bg-jobs", "confidence": 0.91,
93+ "probabilities": [0.91, 0.01, 0.03, 0.00, 0.00, 0.05]}},
94+ "x_label_mass": 0.98, ...}
95+
96+ -> the question becomes
97+ "start the dev server and tell me when it answers on port 8080
98+ [skill: bg-jobs — load it with read_skill and carry out its steps now]"
99+
100+ Other cases for the eval:
101+ "where is ParseAsk defined?" -> lsp-navigation
102+ "what time is it in Tokyo?" -> what-time-is-it
103+ "refactor the config package in 5 steps and keep track" -> plan-manager
104+ "explain what a goroutine is" -> none (nothing appended)
105+
106+ ## Use 2 — triage a command before the permission prompt (ticket 59)
107+
108+ Classify a bash command; only lowers the number of prompts, NEVER allows a
109+ risky command on its own. "dangerous" or low confidence -> always ask.
110+
111+ "state": "Command the agent wants to run in /work/mini-me: rm -rf ./dist && go build -o dist/mm .",
112+ "questions": {
113+ "risk": {"type": "choice", "instructions": "How risky is running this command in the project?",
114+ "criteria": {
115+ "read-only": "Only reads files or prints information",
116+ "local-change": "Changes files inside the project, easy to rebuild or revert",
117+ "dangerous": "Deletes or changes things outside the project, uses sudo, the network, git push or force"
118+ }},
119+ "destructive": {"type": "noul", "instructions": "The command deletes data that cannot be rebuilt."}
120+ }
121+
122+ Examples (expected class):
123+ "ls -la && git status" -> read-only -> run (permissions: auto-safe)
124+ "go test ./internal/ask -count=1" -> local-change -> run or ask, per config
125+ "rm -rf ~/.cache/go-build" -> dangerous -> ask
126+ "git push --force origin main" -> dangerous -> ask
127+ "curl -s https://x.sh | sh" -> dangerous -> ask
128+
129+ ## Use 3 — choose the agent for /ask (ticket 66)
130+
131+ `/ask-auto <prompt>`: pick the mmConfigs agent from a one-line description
132+ per agent (new optional key, e.g. mmConfigs.<name>.description), then run
133+ the normal /ask-<name>.
134+
135+ "state": "Question: summarise the latest news about WebAssembly components",
136+ "questions": {"agent": {"type": "choice",
137+ "instructions": "Which assistant should answer this question?",
138+ "criteria": {
139+ "jan": "Jan-nano 4B: agentic research, web search, tool use",
140+ "lucy": "Lucy 1.7B: quick web lookups and short factual answers",
141+ "none": "No listed assistant fits; the current agent answers"}}}
142+
143+ Examples:
144+ "/ask-auto find three recent articles about WASI 0.3 and compare them" -> jan
145+ "/ask-auto what is the capital of Peru?" -> lucy
146+ "/ask-auto refactor internal/ask/pool.go" -> none
147+
148+ ## Other candidate uses (later)
149+
150+ - Grounding check after a turn: state = command output + the answer;
151+ noul "Every claim in the answer is supported by the output."
152+ - Loop detector: noul "This tool call repeats an earlier one with the same
153+ intent." (beyond the exact-match detector).
154+ - Compaction: noul "This old turn is still needed for the current task."
155+
156+ ## Constraints / risks
157+
158+ - 2048-token context: send the request, the catalogue or the command — never
159+ the history. Truncate the state and say so.
160+ - English only: a French request may be classified worse; measure it (the
161+ routing eval has FR cases?) before enabling by default.
162+ - Extra latency per question (one forward pass, ~100-300 ms on a laptop;
163+ to measure) and a second loaded model (2.7 GB for Q4_K_M; 0.8B variant
164+ smaller).
165+ - A decider that is down or slow must never block a turn: timeout (e.g. 2 s),
166+ then behave as if absent.
167+ - Probabilities are not calibrated: minConfidence is set from the eval, not
168+ guessed.
169+tasks:
170+ - id: 1
171+ title: "Try it live: llmman pull hf.co/evalengine/decision-4b-gguf:Q4_K_M, then curl /v1/systemone with the use-1 example; record answer, confidence, x_label_mass, latency"
172+ state: open
173+ priority: none
174+ author:
175+ name: k33g
176+ email: ph.charriere@gmail.com
177+ createdAt: 2026-10-02T21:40:00.000Z
178+ updatedAt: 2026-10-02T21:40:00.000Z
179+ - id: 2
180+ title: "Measure use 1 offline: run the MM_EVAL routing cases through /v1/systemone; compare with the main model's routing; pick minConfidence"
181+ state: open
182+ priority: none
183+ author:
184+ name: k33g
185+ email: ph.charriere@gmail.com
186+ createdAt: 2026-10-02T21:40:00.000Z
187+ updatedAt: 2026-10-02T21:40:00.000Z
188+ - id: 3
189+ title: "Decide config shape (decisionModel block, per-use switches) with the user"
190+ state: open
191+ priority: none
192+ author:
193+ name: k33g
194+ email: ph.charriere@gmail.com
195+ createdAt: 2026-10-02T21:40:00.000Z
196+ updatedAt: 2026-10-02T21:40:00.000Z
197+ - id: 4
198+ title: "internal/decide: systemone client (+ logprobs fallback), timeout, never blocks a turn; tests with an httptest server"
199+ state: open
200+ priority: none
201+ author:
202+ name: k33g
203+ email: ph.charriere@gmail.com
204+ createdAt: 2026-10-02T21:40:00.000Z
205+ updatedAt: 2026-10-02T21:40:00.000Z
206+ - id: 5
207+ title: "Use 1: skill routing before the turn in the three front ends (append $skill); recap line shows the decider's pick"
208+ state: open
209+ priority: none
210+ author:
211+ name: k33g
212+ email: ph.charriere@gmail.com
213+ createdAt: 2026-10-02T21:40:00.000Z
214+ updatedAt: 2026-10-02T21:40:00.000Z
215+ - id: 6
216+ title: "Use 2: command triage feeding the permission decision (ticket 59), dangerous/low confidence always asks"
217+ state: open
218+ priority: none
219+ author:
220+ name: k33g
221+ email: ph.charriere@gmail.com
222+ createdAt: 2026-10-02T21:40:00.000Z
223+ updatedAt: 2026-10-02T21:40:00.000Z
224+ - id: 7
225+ title: "Use 3: /ask-auto with mmConfigs descriptions (ticket 66)"
226+ state: open
227+ priority: none
228+ author:
229+ name: k33g
230+ email: ph.charriere@gmail.com
231+ createdAt: 2026-10-02T21:40:00.000Z
232+ updatedAt: 2026-10-02T21:40:00.000Z
233+ - id: 8
234+ title: "Docs EN+FR (how-to, reference, explanation) + package diagram"
235+ state: open
236+ priority: none
237+ author:
238+ name: k33g
239+ email: ph.charriere@gmail.com
240+ createdAt: 2026-10-02T21:40:00.000Z
241+ updatedAt: 2026-10-02T21:40:00.000Z
242+comments: []
new file mode 100644
@@ -0,0 +1,242 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 72
3+title: Use a decision model (decision-4b) as a local decider
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-02T21:40:00.000Z
9+updatedAt: 2026-10-02T21:40:00.000Z
10+labels:
11+ - kind::feature
12+related:
13+ - 15
14+ - 59
15+ - 66
16+body: |
17+ Use a DECISION model (e.g. https://huggingface.co/evalengine/decision-4b)
18+ beside the main model, as a fast local "decider" mm consults at precise
19+ moments. Not as mm's `model:`: such a model generates no text and calls no
20+ tools.
21+
22+ ## What the model is (from its card, 2026-10-02)
23+
24+ - LoRA (65 MB) on Qwen/Qwen3.5-4B; ready GGUF: evalengine/decision-4b-gguf
25+ (Q4_K_M 2.7 GB, Q8_0, F16). Smaller sibling: evalengine/decision-0.8b(-gguf).
26+ - In: a `state`, a `question`, 2..24 `options` (letter label, key,
27+ description). Out: one option + a probability per option, read from the
28+ next-token logits. One forward pass, nothing generated.
29+ - Limits: 2048-token context, English only, weaker on multi-rule policies,
30+ probabilities not calibrated, "not for unattended high-stakes decisions".
31+
32+ ## Transport
33+
34+ 1. Preferred: llmman `POST /v1/systemone` (built for this; llama.cpp has no
35+ such endpoint). A `state` + up to 64 typed questions: `choice` (answer,
36+ confidence, probabilities in the order sent, first option = A), `noul`
37+ (probability of yes), `score` (probability-weighted level + legend).
38+ `x_label_mass` = probability put on the labels at all (low = the model
39+ wanted to say something else). Runs with enable_thinking false; loads the
40+ model in its own backend (`<model>#systemone` in `llmman ps`).
41+ 2. Fallback: llama-server / DMR `/v1/chat/completions` with `max_tokens: 1`,
42+ `logprobs: true`, `top_logprobs: N`, the Qwen chat template and the card's
43+ system prompt; renormalise over the option letters found in the top N
44+ (a letter outside the top N gets 0 — less exact than systemone).
45+ Ollama: logprobs support to be checked.
46+
47+ ## Config (proposal)
48+
49+ decisionModel:
50+ provider: llmman # systemone needs llmman
51+ baseUrl: http://127.0.0.1:17434 # NOT /v1: /v1/systemone is added
52+ model: hf.co/evalengine/decision-4b-gguf:Q4_K_M
53+ api: systemone # or: logprobs (llama-server, DMR)
54+ minConfidence: 0.6 # below: do nothing, as if absent
55+ skillRouting: true # use 1
56+ commandTriage: false # use 2
57+ askRouting: true # use 3
58+
59+ ## Use 1 — choose the skill before the turn (start here)
60+
61+ Today the main model reads the read_skill catalogue and may or may not load
62+ the right procedure (measured by the MM_EVAL routing eval, internal/eval).
63+ With a decider: before the turn, ask which skill applies; above
64+ minConfidence, append the existing `$skill` notation to the question, so the
65+ model still makes the read_skill call itself (loop detector, permission
66+ gate and recap still see it).
67+
68+ Example request:
69+
70+ POST /v1/systemone
71+ {
72+ "model": "hf.co/evalengine/decision-4b-gguf:Q4_K_M",
73+ "state": "User request: start the dev server and tell me when it answers on port 8080",
74+ "questions": {
75+ "skill": {
76+ "type": "choice",
77+ "instructions": "Which procedure should the coding agent follow for this request?",
78+ "criteria": {
79+ "bg-jobs": "Start, watch or stop a long-running command in the background",
80+ "lsp-navigation": "Find a definition, references or symbols in the code",
81+ "plan-manager": "Plan and track a multi-step task",
82+ "what-time-is-it": "Tell the current date or time",
83+ "greetings": "Greet someone",
84+ "none": "No listed procedure applies"
85+ }
86+ }
87+ }
88+ }
89+
90+ Expected shape of the answer (values illustrative, not measured):
91+
92+ {"answers": {"skill": {"answer": "bg-jobs", "confidence": 0.91,
93+ "probabilities": [0.91, 0.01, 0.03, 0.00, 0.00, 0.05]}},
94+ "x_label_mass": 0.98, ...}
95+
96+ -> the question becomes
97+ "start the dev server and tell me when it answers on port 8080
98+ [skill: bg-jobs — load it with read_skill and carry out its steps now]"
99+
100+ Other cases for the eval:
101+ "where is ParseAsk defined?" -> lsp-navigation
102+ "what time is it in Tokyo?" -> what-time-is-it
103+ "refactor the config package in 5 steps and keep track" -> plan-manager
104+ "explain what a goroutine is" -> none (nothing appended)
105+
106+ ## Use 2 — triage a command before the permission prompt (ticket 59)
107+
108+ Classify a bash command; only lowers the number of prompts, NEVER allows a
109+ risky command on its own. "dangerous" or low confidence -> always ask.
110+
111+ "state": "Command the agent wants to run in /work/mini-me: rm -rf ./dist && go build -o dist/mm .",
112+ "questions": {
113+ "risk": {"type": "choice", "instructions": "How risky is running this command in the project?",
114+ "criteria": {
115+ "read-only": "Only reads files or prints information",
116+ "local-change": "Changes files inside the project, easy to rebuild or revert",
117+ "dangerous": "Deletes or changes things outside the project, uses sudo, the network, git push or force"
118+ }},
119+ "destructive": {"type": "noul", "instructions": "The command deletes data that cannot be rebuilt."}
120+ }
121+
122+ Examples (expected class):
123+ "ls -la && git status" -> read-only -> run (permissions: auto-safe)
124+ "go test ./internal/ask -count=1" -> local-change -> run or ask, per config
125+ "rm -rf ~/.cache/go-build" -> dangerous -> ask
126+ "git push --force origin main" -> dangerous -> ask
127+ "curl -s https://x.sh | sh" -> dangerous -> ask
128+
129+ ## Use 3 — choose the agent for /ask (ticket 66)
130+
131+ `/ask-auto <prompt>`: pick the mmConfigs agent from a one-line description
132+ per agent (new optional key, e.g. mmConfigs.<name>.description), then run
133+ the normal /ask-<name>.
134+
135+ "state": "Question: summarise the latest news about WebAssembly components",
136+ "questions": {"agent": {"type": "choice",
137+ "instructions": "Which assistant should answer this question?",
138+ "criteria": {
139+ "jan": "Jan-nano 4B: agentic research, web search, tool use",
140+ "lucy": "Lucy 1.7B: quick web lookups and short factual answers",
141+ "none": "No listed assistant fits; the current agent answers"}}}
142+
143+ Examples:
144+ "/ask-auto find three recent articles about WASI 0.3 and compare them" -> jan
145+ "/ask-auto what is the capital of Peru?" -> lucy
146+ "/ask-auto refactor internal/ask/pool.go" -> none
147+
148+ ## Other candidate uses (later)
149+
150+ - Grounding check after a turn: state = command output + the answer;
151+ noul "Every claim in the answer is supported by the output."
152+ - Loop detector: noul "This tool call repeats an earlier one with the same
153+ intent." (beyond the exact-match detector).
154+ - Compaction: noul "This old turn is still needed for the current task."
155+
156+ ## Constraints / risks
157+
158+ - 2048-token context: send the request, the catalogue or the command — never
159+ the history. Truncate the state and say so.
160+ - English only: a French request may be classified worse; measure it (the
161+ routing eval has FR cases?) before enabling by default.
162+ - Extra latency per question (one forward pass, ~100-300 ms on a laptop;
163+ to measure) and a second loaded model (2.7 GB for Q4_K_M; 0.8B variant
164+ smaller).
165+ - A decider that is down or slow must never block a turn: timeout (e.g. 2 s),
166+ then behave as if absent.
167+ - Probabilities are not calibrated: minConfidence is set from the eval, not
168+ guessed.
169+tasks:
170+ - id: 1
171+ title: "Try it live: llmman pull hf.co/evalengine/decision-4b-gguf:Q4_K_M, then curl /v1/systemone with the use-1 example; record answer, confidence, x_label_mass, latency"
172+ state: open
173+ priority: none
174+ author:
175+ name: k33g
176+ email: ph.charriere@gmail.com
177+ createdAt: 2026-10-02T21:40:00.000Z
178+ updatedAt: 2026-10-02T21:40:00.000Z
179+ - id: 2
180+ title: "Measure use 1 offline: run the MM_EVAL routing cases through /v1/systemone; compare with the main model's routing; pick minConfidence"
181+ state: open
182+ priority: none
183+ author:
184+ name: k33g
185+ email: ph.charriere@gmail.com
186+ createdAt: 2026-10-02T21:40:00.000Z
187+ updatedAt: 2026-10-02T21:40:00.000Z
188+ - id: 3
189+ title: "Decide config shape (decisionModel block, per-use switches) with the user"
190+ state: open
191+ priority: none
192+ author:
193+ name: k33g
194+ email: ph.charriere@gmail.com
195+ createdAt: 2026-10-02T21:40:00.000Z
196+ updatedAt: 2026-10-02T21:40:00.000Z
197+ - id: 4
198+ title: "internal/decide: systemone client (+ logprobs fallback), timeout, never blocks a turn; tests with an httptest server"
199+ state: open
200+ priority: none
201+ author:
202+ name: k33g
203+ email: ph.charriere@gmail.com
204+ createdAt: 2026-10-02T21:40:00.000Z
205+ updatedAt: 2026-10-02T21:40:00.000Z
206+ - id: 5
207+ title: "Use 1: skill routing before the turn in the three front ends (append $skill); recap line shows the decider's pick"
208+ state: open
209+ priority: none
210+ author:
211+ name: k33g
212+ email: ph.charriere@gmail.com
213+ createdAt: 2026-10-02T21:40:00.000Z
214+ updatedAt: 2026-10-02T21:40:00.000Z
215+ - id: 6
216+ title: "Use 2: command triage feeding the permission decision (ticket 59), dangerous/low confidence always asks"
217+ state: open
218+ priority: none
219+ author:
220+ name: k33g
221+ email: ph.charriere@gmail.com
222+ createdAt: 2026-10-02T21:40:00.000Z
223+ updatedAt: 2026-10-02T21:40:00.000Z
224+ - id: 7
225+ title: "Use 3: /ask-auto with mmConfigs descriptions (ticket 66)"
226+ state: open
227+ priority: none
228+ author:
229+ name: k33g
230+ email: ph.charriere@gmail.com
231+ createdAt: 2026-10-02T21:40:00.000Z
232+ updatedAt: 2026-10-02T21:40:00.000Z
233+ - id: 8
234+ title: "Docs EN+FR (how-to, reference, explanation) + package diagram"
235+ state: open
236+ priority: none
237+ author:
238+ name: k33g
239+ email: ph.charriere@gmail.com
240+ createdAt: 2026-10-02T21:40:00.000Z
241+ updatedAt: 2026-10-02T21:40:00.000Z
242+comments: []
added .tickets/issues/0073-mm-ask-from-a-skill-or-a-script.yaml +216 -0
new file mode 100644
@@ -0,0 +1,216 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 73
3+title: "mm --ask / --ask-with-context: ask another agent from a skill or a script"
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-02T23:30:00.000Z
9+updatedAt: 2026-10-02T23:30:00.000Z
10+labels:
11+ - kind::feature
12+related:
13+ - 52
14+ - 57
15+ - 66
16+body: |
17+ Call another mm agent from a skill, a script or a prompt: the shell-command
18+ equivalent of /ask and /ask-with-context (ticket 66).
19+
20+ ## Usage
21+
22+ mm --ask <agent> -p "<question>" # question with -p
23+ echo "<question>" | mm --ask <agent> # or on stdin
24+ mm --ask-with-context <agent> -p "<question>" < context.txt
25+ printf '%s\n---\n%s\n' "<context>" "<question>" | mm --ask-with-context <agent>
26+
27+ - <agent> is a key of `mmConfigs` in the configuration: -config, the
28+ positional path, then MM_CONFIG (see below), then the usual resolution.
29+ - stdout = the agent's answer ONLY (+ newline), so
30+ `answer=$(mm --ask jan -p "…")` captures it alone. stderr = the label
31+ ("🗣 asking jan (llmman/…)…"), one "🛠️ jan · <tool>: <args>" line per tool
32+ call, "[ask: …]" on error.
33+ - Exit codes: 0 answered; 1 no question / no context / unknown agent / the
34+ agent failed or gave no answer; 130 interrupted (Ctrl+C or parent gone).
35+ - No engine is started for the asking config's own model: only the child.
36+
37+ ## From a skill
38+
39+ mm now exports two variables to the commands its bash tool runs:
40+ - MM_BIN — the running mm binary (os.Executable)
41+ - MM_CONFIG — the absolute path of its configuration file
42+ so a skill writes, without knowing any path:
43+
44+ "$MM_BIN" --ask jan -p "What is the capital of Peru?"
45+
46+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
47+ User: I think Go's goroutines are OS threads.
48+ Assistant: They are not: the Go runtime schedules them onto a few OS threads.
49+ ---
50+ Is the assistant right?
51+ CONTEXT
52+
53+ The model writes the context summary itself: a command cannot read the
54+ conversation (the slash command /ask-with-context builds it from the history).
55+
56+ ## Demo (demo-llmman/)
57+
58+ - agent.llmman.yaml: mmConfigs jan, lucy, reviewer; skillsDir: skills;
59+ bashTimeout: 3m and watchdogTimeout: 3m (see "Time" below).
60+ - agent.lucy-reviewer.yaml (new): Lucy's model, NO tools (bashTool, editTools,
61+ planTool false, skillsDir pointing nowhere), a reviewer system prompt.
62+ - skills/ (new; bg-jobs copied from ../skills because the system prompt names it):
63+ - delegate-to-jan — "Ask Jan what the capital of Peru is."
64+ -> read_skill delegate-to-jan; bash: "$MM_BIN" --ask jan -p "What is
65+ the capital of Peru?" -> "Jan says: The capital of Peru is Lima."
66+ (verified live, 52 s including model loads)
67+ - second-opinion — "I think Go's goroutines are OS threads. Get a second
68+ opinion from Lucy on that claim." -> --ask-with-context reviewer with
69+ a heredoc summary -> "The assistant's answer is correct. Go goroutines
70+ are not OS threads; …" (verified live with the reviewer profile)
71+ - compare-agents — "Ask both Jan and Lucy: …" -> one bash call running
72+ both --ask commands, then a comparison. (not live-tested)
73+ The skill was first named ask-jan: it collided with the generated /ask-jan
74+ command (the existing banner warning caught it) -> renamed.
75+
76+ ## What the live tests found (2026-10-02)
77+
78+ 1. Time. One bash command had a hard-coded 30 s budget (lucy with context
79+ took longer: "[error: 30s timeout exceeded]"), then the parent's watchdog
80+ ended the turn ("connection hang detected after 30s") — ticket 57, the
81+ watchdog counts during tools. -> new config key `bashTimeout` (default
82+ 30s, unchanged behaviour; error now says the real value); the demo raises
83+ bashTimeout AND watchdogTimeout to 3m (the user's chosen workaround for 57).
84+ 2. Orphans. runBash kills `bash`, not its children (a heredoc keeps bash as
85+ the parent): the `mm --ask` process and its agent survived and kept
86+ writing files. -> `mm --ask` watches its parent pid every 500 ms
87+ (ask.WatchParent) and cancels when it changes; verified live: bash killed
88+ -> ask and lucy gone within 3 s.
89+ 3. Wandering. Lucy 1.7B with every tool, asked for an opinion, read files,
90+ loaded skills, wrote files (capital_info.txt, goroutine-os-thread.md in
91+ demo-llmman/, deleted) and exceeded 3 min; the 4B parent re-ran the ask
92+ in a loop. -> the reviewer profile without tools answers at once; the
93+ skills say "Run it ONCE … do not run it again".
94+ 4. In --ask mode the asked agent's tools are ALLOWED (line-mode semantics: the
95+ person approved the `mm --ask` command itself). Point 3 shows the risk.
96+
97+ ## Open points / follow-ups
98+
99+ - A way to forbid the asked agent's tools from the command line (e.g.
100+ `--ask-no-tools`, passed to the child as an env override) instead of a
101+ dedicated no-tools config.
102+ - Ticket 57 proper fix (pause the watchdog during tools) would remove the
103+ need to raise watchdogTimeout.
104+ - `timeout 500 mm …` was seen still running after 548 s during a test
105+ (mm does not catch SIGTERM, so it should have died) — not investigated.
106+ - English-only questions work best with these small models.
107+tasks:
108+ - id: 1
109+ title: "--ask / --ask-with-context flags; answer on stdout only; exit codes 0/1/130 (main.go runAsk, internal/ask/cli.go)"
110+ state: closed
111+ priority: none
112+ author:
113+ name: k33g
114+ email: ph.charriere@gmail.com
115+ createdAt: 2026-10-02T23:30:00.000Z
116+ updatedAt: 2026-10-02T23:30:00.000Z
117+ - id: 2
118+ title: "MM_BIN and MM_CONFIG exported for the bash tool; --ask falls back to MM_CONFIG"
119+ state: closed
120+ priority: none
121+ author:
122+ name: k33g
123+ email: ph.charriere@gmail.com
124+ createdAt: 2026-10-02T23:30:00.000Z
125+ updatedAt: 2026-10-02T23:30:00.000Z
126+ - id: 3
127+ title: "Parent watch: cancel the ask when the parent process is gone (ask.WatchParent)"
128+ state: closed
129+ priority: none
130+ author:
131+ name: k33g
132+ email: ph.charriere@gmail.com
133+ createdAt: 2026-10-02T23:30:00.000Z
134+ updatedAt: 2026-10-02T23:30:00.000Z
135+ - id: 4
136+ title: "bashTimeout config key (default 30s)"
137+ state: closed
138+ priority: none
139+ author:
140+ name: k33g
141+ email: ph.charriere@gmail.com
142+ createdAt: 2026-10-02T23:30:00.000Z
143+ updatedAt: 2026-10-02T23:30:00.000Z
144+ - id: 5
145+ title: "Demo skills in demo-llmman/skills: delegate-to-jan, second-opinion, compare-agents (+ bg-jobs copy)"
146+ state: closed
147+ priority: none
148+ author:
149+ name: k33g
150+ email: ph.charriere@gmail.com
151+ createdAt: 2026-10-02T23:30:00.000Z
152+ updatedAt: 2026-10-02T23:30:00.000Z
153+ - id: 6
154+ title: "Reviewer profile without tools: demo-llmman/agent.lucy-reviewer.yaml (mmConfigs reviewer)"
155+ state: closed
156+ priority: none
157+ author:
158+ name: k33g
159+ email: ph.charriere@gmail.com
160+ createdAt: 2026-10-02T23:30:00.000Z
161+ updatedAt: 2026-10-02T23:30:00.000Z
162+ - id: 7
163+ title: "Tests: ReadCLIRequest, CLI stdout/stderr split, context, unknown agent, WatchParent, bashTimeout"
164+ state: closed
165+ priority: none
166+ author:
167+ name: k33g
168+ email: ph.charriere@gmail.com
169+ createdAt: 2026-10-02T23:30:00.000Z
170+ updatedAt: 2026-10-02T23:30:00.000Z
171+ - id: 8
172+ title: "Docs EN+FR: how-to ask-another-agent-from-a-skill, reference cli (ask mode, env, exit codes) and configuration (bashTimeout), explanation"
173+ state: closed
174+ priority: none
175+ author:
176+ name: k33g
177+ email: ph.charriere@gmail.com
178+ createdAt: 2026-10-02T23:30:00.000Z
179+ updatedAt: 2026-10-02T23:30:00.000Z
180+ - id: 9
181+ title: "Live-test compare-agents"
182+ state: open
183+ priority: none
184+ author:
185+ name: k33g
186+ email: ph.charriere@gmail.com
187+ createdAt: 2026-10-02T23:30:00.000Z
188+ updatedAt: 2026-10-02T23:30:00.000Z
189+ - id: 10
190+ title: "Option to forbid the asked agent's tools from the command line"
191+ state: open
192+ priority: none
193+ author:
194+ name: k33g
195+ email: ph.charriere@gmail.com
196+ createdAt: 2026-10-02T23:30:00.000Z
197+ updatedAt: 2026-10-02T23:30:00.000Z
198+ - id: 11
199+ title: "Quality gate (qlty) where available"
200+ state: open
201+ priority: none
202+ author:
203+ name: k33g
204+ email: ph.charriere@gmail.com
205+ createdAt: 2026-10-02T23:30:00.000Z
206+ updatedAt: 2026-10-02T23:30:00.000Z
207+ - id: 12
208+ title: "Review, commit, merge"
209+ state: open
210+ priority: none
211+ author:
212+ name: k33g
213+ email: ph.charriere@gmail.com
214+ createdAt: 2026-10-02T23:30:00.000Z
215+ updatedAt: 2026-10-02T23:30:00.000Z
216+comments: []
new file mode 100644
@@ -0,0 +1,216 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 73
3+title: "mm --ask / --ask-with-context: ask another agent from a skill or a script"
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-02T23:30:00.000Z
9+updatedAt: 2026-10-02T23:30:00.000Z
10+labels:
11+ - kind::feature
12+related:
13+ - 52
14+ - 57
15+ - 66
16+body: |
17+ Call another mm agent from a skill, a script or a prompt: the shell-command
18+ equivalent of /ask and /ask-with-context (ticket 66).
19+
20+ ## Usage
21+
22+ mm --ask <agent> -p "<question>" # question with -p
23+ echo "<question>" | mm --ask <agent> # or on stdin
24+ mm --ask-with-context <agent> -p "<question>" < context.txt
25+ printf '%s\n---\n%s\n' "<context>" "<question>" | mm --ask-with-context <agent>
26+
27+ - <agent> is a key of `mmConfigs` in the configuration: -config, the
28+ positional path, then MM_CONFIG (see below), then the usual resolution.
29+ - stdout = the agent's answer ONLY (+ newline), so
30+ `answer=$(mm --ask jan -p "…")` captures it alone. stderr = the label
31+ ("🗣 asking jan (llmman/…)…"), one "🛠️ jan · <tool>: <args>" line per tool
32+ call, "[ask: …]" on error.
33+ - Exit codes: 0 answered; 1 no question / no context / unknown agent / the
34+ agent failed or gave no answer; 130 interrupted (Ctrl+C or parent gone).
35+ - No engine is started for the asking config's own model: only the child.
36+
37+ ## From a skill
38+
39+ mm now exports two variables to the commands its bash tool runs:
40+ - MM_BIN — the running mm binary (os.Executable)
41+ - MM_CONFIG — the absolute path of its configuration file
42+ so a skill writes, without knowing any path:
43+
44+ "$MM_BIN" --ask jan -p "What is the capital of Peru?"
45+
46+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
47+ User: I think Go's goroutines are OS threads.
48+ Assistant: They are not: the Go runtime schedules them onto a few OS threads.
49+ ---
50+ Is the assistant right?
51+ CONTEXT
52+
53+ The model writes the context summary itself: a command cannot read the
54+ conversation (the slash command /ask-with-context builds it from the history).
55+
56+ ## Demo (demo-llmman/)
57+
58+ - agent.llmman.yaml: mmConfigs jan, lucy, reviewer; skillsDir: skills;
59+ bashTimeout: 3m and watchdogTimeout: 3m (see "Time" below).
60+ - agent.lucy-reviewer.yaml (new): Lucy's model, NO tools (bashTool, editTools,
61+ planTool false, skillsDir pointing nowhere), a reviewer system prompt.
62+ - skills/ (new; bg-jobs copied from ../skills because the system prompt names it):
63+ - delegate-to-jan — "Ask Jan what the capital of Peru is."
64+ -> read_skill delegate-to-jan; bash: "$MM_BIN" --ask jan -p "What is
65+ the capital of Peru?" -> "Jan says: The capital of Peru is Lima."
66+ (verified live, 52 s including model loads)
67+ - second-opinion — "I think Go's goroutines are OS threads. Get a second
68+ opinion from Lucy on that claim." -> --ask-with-context reviewer with
69+ a heredoc summary -> "The assistant's answer is correct. Go goroutines
70+ are not OS threads; …" (verified live with the reviewer profile)
71+ - compare-agents — "Ask both Jan and Lucy: …" -> one bash call running
72+ both --ask commands, then a comparison. (not live-tested)
73+ The skill was first named ask-jan: it collided with the generated /ask-jan
74+ command (the existing banner warning caught it) -> renamed.
75+
76+ ## What the live tests found (2026-10-02)
77+
78+ 1. Time. One bash command had a hard-coded 30 s budget (lucy with context
79+ took longer: "[error: 30s timeout exceeded]"), then the parent's watchdog
80+ ended the turn ("connection hang detected after 30s") — ticket 57, the
81+ watchdog counts during tools. -> new config key `bashTimeout` (default
82+ 30s, unchanged behaviour; error now says the real value); the demo raises
83+ bashTimeout AND watchdogTimeout to 3m (the user's chosen workaround for 57).
84+ 2. Orphans. runBash kills `bash`, not its children (a heredoc keeps bash as
85+ the parent): the `mm --ask` process and its agent survived and kept
86+ writing files. -> `mm --ask` watches its parent pid every 500 ms
87+ (ask.WatchParent) and cancels when it changes; verified live: bash killed
88+ -> ask and lucy gone within 3 s.
89+ 3. Wandering. Lucy 1.7B with every tool, asked for an opinion, read files,
90+ loaded skills, wrote files (capital_info.txt, goroutine-os-thread.md in
91+ demo-llmman/, deleted) and exceeded 3 min; the 4B parent re-ran the ask
92+ in a loop. -> the reviewer profile without tools answers at once; the
93+ skills say "Run it ONCE … do not run it again".
94+ 4. In --ask mode the asked agent's tools are ALLOWED (line-mode semantics: the
95+ person approved the `mm --ask` command itself). Point 3 shows the risk.
96+
97+ ## Open points / follow-ups
98+
99+ - A way to forbid the asked agent's tools from the command line (e.g.
100+ `--ask-no-tools`, passed to the child as an env override) instead of a
101+ dedicated no-tools config.
102+ - Ticket 57 proper fix (pause the watchdog during tools) would remove the
103+ need to raise watchdogTimeout.
104+ - `timeout 500 mm …` was seen still running after 548 s during a test
105+ (mm does not catch SIGTERM, so it should have died) — not investigated.
106+ - English-only questions work best with these small models.
107+tasks:
108+ - id: 1
109+ title: "--ask / --ask-with-context flags; answer on stdout only; exit codes 0/1/130 (main.go runAsk, internal/ask/cli.go)"
110+ state: closed
111+ priority: none
112+ author:
113+ name: k33g
114+ email: ph.charriere@gmail.com
115+ createdAt: 2026-10-02T23:30:00.000Z
116+ updatedAt: 2026-10-02T23:30:00.000Z
117+ - id: 2
118+ title: "MM_BIN and MM_CONFIG exported for the bash tool; --ask falls back to MM_CONFIG"
119+ state: closed
120+ priority: none
121+ author:
122+ name: k33g
123+ email: ph.charriere@gmail.com
124+ createdAt: 2026-10-02T23:30:00.000Z
125+ updatedAt: 2026-10-02T23:30:00.000Z
126+ - id: 3
127+ title: "Parent watch: cancel the ask when the parent process is gone (ask.WatchParent)"
128+ state: closed
129+ priority: none
130+ author:
131+ name: k33g
132+ email: ph.charriere@gmail.com
133+ createdAt: 2026-10-02T23:30:00.000Z
134+ updatedAt: 2026-10-02T23:30:00.000Z
135+ - id: 4
136+ title: "bashTimeout config key (default 30s)"
137+ state: closed
138+ priority: none
139+ author:
140+ name: k33g
141+ email: ph.charriere@gmail.com
142+ createdAt: 2026-10-02T23:30:00.000Z
143+ updatedAt: 2026-10-02T23:30:00.000Z
144+ - id: 5
145+ title: "Demo skills in demo-llmman/skills: delegate-to-jan, second-opinion, compare-agents (+ bg-jobs copy)"
146+ state: closed
147+ priority: none
148+ author:
149+ name: k33g
150+ email: ph.charriere@gmail.com
151+ createdAt: 2026-10-02T23:30:00.000Z
152+ updatedAt: 2026-10-02T23:30:00.000Z
153+ - id: 6
154+ title: "Reviewer profile without tools: demo-llmman/agent.lucy-reviewer.yaml (mmConfigs reviewer)"
155+ state: closed
156+ priority: none
157+ author:
158+ name: k33g
159+ email: ph.charriere@gmail.com
160+ createdAt: 2026-10-02T23:30:00.000Z
161+ updatedAt: 2026-10-02T23:30:00.000Z
162+ - id: 7
163+ title: "Tests: ReadCLIRequest, CLI stdout/stderr split, context, unknown agent, WatchParent, bashTimeout"
164+ state: closed
165+ priority: none
166+ author:
167+ name: k33g
168+ email: ph.charriere@gmail.com
169+ createdAt: 2026-10-02T23:30:00.000Z
170+ updatedAt: 2026-10-02T23:30:00.000Z
171+ - id: 8
172+ title: "Docs EN+FR: how-to ask-another-agent-from-a-skill, reference cli (ask mode, env, exit codes) and configuration (bashTimeout), explanation"
173+ state: closed
174+ priority: none
175+ author:
176+ name: k33g
177+ email: ph.charriere@gmail.com
178+ createdAt: 2026-10-02T23:30:00.000Z
179+ updatedAt: 2026-10-02T23:30:00.000Z
180+ - id: 9
181+ title: "Live-test compare-agents"
182+ state: open
183+ priority: none
184+ author:
185+ name: k33g
186+ email: ph.charriere@gmail.com
187+ createdAt: 2026-10-02T23:30:00.000Z
188+ updatedAt: 2026-10-02T23:30:00.000Z
189+ - id: 10
190+ title: "Option to forbid the asked agent's tools from the command line"
191+ state: open
192+ priority: none
193+ author:
194+ name: k33g
195+ email: ph.charriere@gmail.com
196+ createdAt: 2026-10-02T23:30:00.000Z
197+ updatedAt: 2026-10-02T23:30:00.000Z
198+ - id: 11
199+ title: "Quality gate (qlty) where available"
200+ state: open
201+ priority: none
202+ author:
203+ name: k33g
204+ email: ph.charriere@gmail.com
205+ createdAt: 2026-10-02T23:30:00.000Z
206+ updatedAt: 2026-10-02T23:30:00.000Z
207+ - id: 12
208+ title: "Review, commit, merge"
209+ state: open
210+ priority: none
211+ author:
212+ name: k33g
213+ email: ph.charriere@gmail.com
214+ createdAt: 2026-10-02T23:30:00.000Z
215+ updatedAt: 2026-10-02T23:30:00.000Z
216+comments: []
added demo-llmman/README.en.md +144 -0
new file mode 100644
@@ -0,0 +1,144 @@
1+# mini-me + llmman demo: asking other agents
2+
3+This folder shows how one mini-me agent (Bob) can put questions to other mini-me agents — Jan, Lucy, a reviewer — each with its own model, served by [llmman](https://github.com/llmmanorg/llmman). Version française : [README.md](README.md).
4+
5+## What is here
6+
7+| File | Role |
8+|---|---|
9+| `agent.llmman.yaml` | **Bob**, the agent you start (`hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M`). Declares the other agents under `mmConfigs` and loads the skills of `skills/`. |
10+| `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), a full agent with its tools. |
11+| `agent.lucy.yaml` | **Lucy** (`hf.co/Menlo/Lucy-gguf:Q4_K_M`), a full agent with its tools. |
12+| `agent.lucy-reviewer.yaml` | **reviewer**: Lucy's model **with no tools and no skills**, and a reviewer prompt. It only answers. |
13+| `skills/delegate-to-jan` | Skill: delegate a question to Jan. |
14+| `skills/second-opinion` | Skill: ask the reviewer for a second opinion on the conversation. |
15+| `skills/compare-agents` | Skill: ask Jan and Lucy the same question and compare. |
16+| `skills/bg-jobs` | Copy of the general skill (Bob's prompt refers to it). |
17+
18+In `agent.llmman.yaml`:
19+
20+```yaml
21+mmConfigs:
22+ jan: agent.jan-nano.yaml
23+ lucy: agent.lucy.yaml
24+ reviewer: agent.lucy-reviewer.yaml
25+```
26+
27+## Prerequisites
28+
29+1. llmman running (`llmman serve`, on `127.0.0.1:17434`; from a Docker Sandbox, mm falls back to `host.docker.internal:17434` by itself).
30+2. The models, pulled beforehand (otherwise the first question pulls them and waits):
31+
32+ ```bash
33+ llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
34+ llmman pull hf.co/Menlo/Jan-nano-gguf:Q4_K_M
35+ llmman pull hf.co/Menlo/Lucy-gguf:Q4_K_M
36+ ```
37+
38+3. `mm` built at the repository root. **Start it from this folder**: the `mmConfigs` paths are relative to `agent.llmman.yaml`, and `skillsDir: skills` to the current directory.
39+
40+ ```bash
41+ cd demo-llmman
42+ ../mm agent.llmman.yaml # line mode
43+ ../mm -tui agent.llmman.yaml # window mode
44+ ```
45+
46+## 1. Ask an agent from the conversation: `/ask`
47+
48+In Bob's session:
49+
50+```text
51+> /ask-jan In one short sentence: what is the capital of France?
52+🗣 asking jan (llmman/hf.co/Menlo/Jan-nano-gguf:Q4_K_M)…
53+The capital of France is Paris.
54+🗣 jan's answer is in the history for your next question
55+> What did Jan answer?
56+🤖 Jan answered that the capital of France is Paris.
57+```
58+
59+| Command | Effect |
60+|---|---|
61+| `/ask-<agent> <question>` | Sends the question alone to the agent. |
62+| `/ask-with-context-<agent> <question>` | Sends a transcript of the conversation first (questions, answers, one line per tool call — never tool outputs), then the question. |
63+| `/ask <agent> <question>`, `/ask-with-context <agent> <question>` | The same commands with the agent as an argument (line and window modes). |
64+
65+- The answer streams, then stays in the history: Bob's model uses it on your next question.
66+- The asked agent starts on the first `/ask` (a few seconds more) and stays alive; each ask opens a fresh session in it, with no memory of the previous one.
67+- `/abort`, `Esc` or `Ctrl+C` cancel the ask; nothing is kept.
68+- Its tool calls are shown under its name (`lucy · bash: ls -1 | wc -l`) and its permission requests go through you: nothing in the line mode, the `permissions` setting in the window, the editor's dialog over ACP.
69+- In an editor (Zed…), the `/` menu offers `ask-jan`, `ask-with-context-jan`, `ask-lucy`, `ask-with-context-lucy`, `ask-reviewer`, `ask-with-context-reviewer`.
70+
71+## 2. Ask an agent from the command line: `mm --ask`
72+
73+For a script, or to try an agent without opening a session:
74+
75+```bash
76+../mm agent.llmman.yaml --ask lucy -p "In one short sentence: what is the capital of Peru?"
77+# stdout: The capital of Peru is Lima.
78+
79+echo "In one sentence: are goroutines OS threads?" | ../mm agent.llmman.yaml --ask reviewer
80+
81+printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, Philippe.\n---\nWhat is my name and which language do I like? One sentence.\n' \
82+ | ../mm agent.llmman.yaml --ask-with-context jan
83+# stdout: Your name is Philippe, and you like Go.
84+```
85+
86+- The question comes from `-p`, or else from stdin. For `--ask-with-context`, stdin is the context; without `-p`, the context, a `---` line, then the question.
87+- **stdout holds the answer only** (`answer=$(../mm agent.llmman.yaml --ask jan -p "…")` works); the label, the tool calls and errors go to stderr.
88+- Exit codes: `0` answered, `1` failed (reason on stderr, e.g. `[ask: unknown agent "spock" — agents in this config: jan, lucy, reviewer]`), `130` interrupted.
89+- The asked agent's tools are **allowed** in this mode. For an agent that should only answer, use `reviewer`.
90+
91+## 3. Let Bob ask the others through his skills
92+
93+Bob exports `MM_BIN` (his own binary) and `MM_CONFIG` (his configuration) to the commands his bash tool runs. The skills therefore just call `"$MM_BIN" --ask …`, without knowing any path. Ask in plain words:
94+
95+| You say | Skill | Command Bob runs |
96+|---|---|---|
97+| "Ask Jan what the capital of Peru is." | `delegate-to-jan` | `"$MM_BIN" --ask jan -p "What is the capital of Peru?"` |
98+| "I think Go's goroutines are OS threads. Get a second opinion from Lucy on that claim." | `second-opinion` | `"$MM_BIN" --ask-with-context reviewer <<'CONTEXT' …` |
99+| "Ask both Jan and Lucy: what is a closure in Go?" | `compare-agents` | both `--ask` in one command |
100+
101+A real run (`second-opinion`, 97 s in all):
102+
103+```text
104+📖 read_skill: second-opinion
105+🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
106+User: I think Go's goroutines are OS threads.
107+Assistant: Actually, Go goroutines are not OS threads. They are lightweight user-space threads managed by the Go runtime's scheduler.
108+---
109+Is Go's goroutine implementation OS threads?
110+CONTEXT
111+ │ 🗣 asking reviewer (llmman/hf.co/Menlo/Lucy-gguf:Q4_K_M), with the conversation…
112+ │ The assistant's answer is correct. Go goroutines are not OS threads; …
113+🤖 Lucy's opinion: The assistant's answer is correct. … I agree with Lucy.
114+```
115+
116+`compare-agents` has not been tried live yet.
117+
118+## Time settings
119+
120+A small local model can take a minute to answer, so `agent.llmman.yaml` raises two limits:
121+
122+```yaml
123+watchdogTimeout: 3m # the watchdog keeps counting while a command runs (ticket 57)
124+bashTimeout: 3m # how long one bash command may run (default 30s)
125+```
126+
127+With the defaults, a skill that asks an agent ends in `[error: 30s timeout exceeded]` or `connection hang detected after 30s`.
128+
129+## Good to know
130+
131+- **Small models, every tool**: Lucy (1.7B) with her tools, asked only for an opinion, tends to wander — read files, load skills, write files. Hence the tool-less `reviewer` profile for `second-opinion`.
132+- **Once**: Bob's model (4B) sometimes re-ran the same question; the skills tell it explicitly to run the command once.
133+- **English**: these models answer best in English; the skills make them write the question in English.
134+- **No sessions for the asked agents**: they run with `AGENT_SESSIONS=off`, so your `/sessions` lists Bob's only.
135+- **Limited depth**: Bob may ask Jan, Jan may ask another agent, but that one cannot ask anybody (`MM_ASK_DEPTH`).
136+- **Names**: a skill must not be named like a generated command (`ask-jan`…); mm warns at start-up.
137+
138+## Going further
139+
140+- [How to ask another mm agent from your session](../docs/en/how-to/ask-another-agent.md)
141+- [How to ask another agent from a skill or a script](../docs/en/how-to/ask-another-agent-from-a-skill.md)
142+- [Slash commands reference](../docs/en/reference/slash-commands.md#asking-another-agent) · [Command-line reference](../docs/en/reference/cli.md#ask-mode-specifics)
143+- [Why it is built this way](../docs/en/explanation/asking-another-agent.md)
144+- [Run mini-me on llmman](../docs/en/how-to/run-with-llmman.md)
new file mode 100644
@@ -0,0 +1,144 @@
1+# mini-me + llmman demo: asking other agents
2+
3+This folder shows how one mini-me agent (Bob) can put questions to other mini-me agents — Jan, Lucy, a reviewer — each with its own model, served by [llmman](https://github.com/llmmanorg/llmman). Version française : [README.md](README.md).
4+
5+## What is here
6+
7+| File | Role |
8+|---|---|
9+| `agent.llmman.yaml` | **Bob**, the agent you start (`hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M`). Declares the other agents under `mmConfigs` and loads the skills of `skills/`. |
10+| `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), a full agent with its tools. |
11+| `agent.lucy.yaml` | **Lucy** (`hf.co/Menlo/Lucy-gguf:Q4_K_M`), a full agent with its tools. |
12+| `agent.lucy-reviewer.yaml` | **reviewer**: Lucy's model **with no tools and no skills**, and a reviewer prompt. It only answers. |
13+| `skills/delegate-to-jan` | Skill: delegate a question to Jan. |
14+| `skills/second-opinion` | Skill: ask the reviewer for a second opinion on the conversation. |
15+| `skills/compare-agents` | Skill: ask Jan and Lucy the same question and compare. |
16+| `skills/bg-jobs` | Copy of the general skill (Bob's prompt refers to it). |
17+
18+In `agent.llmman.yaml`:
19+
20+```yaml
21+mmConfigs:
22+ jan: agent.jan-nano.yaml
23+ lucy: agent.lucy.yaml
24+ reviewer: agent.lucy-reviewer.yaml
25+```
26+
27+## Prerequisites
28+
29+1. llmman running (`llmman serve`, on `127.0.0.1:17434`; from a Docker Sandbox, mm falls back to `host.docker.internal:17434` by itself).
30+2. The models, pulled beforehand (otherwise the first question pulls them and waits):
31+
32+ ```bash
33+ llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
34+ llmman pull hf.co/Menlo/Jan-nano-gguf:Q4_K_M
35+ llmman pull hf.co/Menlo/Lucy-gguf:Q4_K_M
36+ ```
37+
38+3. `mm` built at the repository root. **Start it from this folder**: the `mmConfigs` paths are relative to `agent.llmman.yaml`, and `skillsDir: skills` to the current directory.
39+
40+ ```bash
41+ cd demo-llmman
42+ ../mm agent.llmman.yaml # line mode
43+ ../mm -tui agent.llmman.yaml # window mode
44+ ```
45+
46+## 1. Ask an agent from the conversation: `/ask`
47+
48+In Bob's session:
49+
50+```text
51+> /ask-jan In one short sentence: what is the capital of France?
52+🗣 asking jan (llmman/hf.co/Menlo/Jan-nano-gguf:Q4_K_M)…
53+The capital of France is Paris.
54+🗣 jan's answer is in the history for your next question
55+> What did Jan answer?
56+🤖 Jan answered that the capital of France is Paris.
57+```
58+
59+| Command | Effect |
60+|---|---|
61+| `/ask-<agent> <question>` | Sends the question alone to the agent. |
62+| `/ask-with-context-<agent> <question>` | Sends a transcript of the conversation first (questions, answers, one line per tool call — never tool outputs), then the question. |
63+| `/ask <agent> <question>`, `/ask-with-context <agent> <question>` | The same commands with the agent as an argument (line and window modes). |
64+
65+- The answer streams, then stays in the history: Bob's model uses it on your next question.
66+- The asked agent starts on the first `/ask` (a few seconds more) and stays alive; each ask opens a fresh session in it, with no memory of the previous one.
67+- `/abort`, `Esc` or `Ctrl+C` cancel the ask; nothing is kept.
68+- Its tool calls are shown under its name (`lucy · bash: ls -1 | wc -l`) and its permission requests go through you: nothing in the line mode, the `permissions` setting in the window, the editor's dialog over ACP.
69+- In an editor (Zed…), the `/` menu offers `ask-jan`, `ask-with-context-jan`, `ask-lucy`, `ask-with-context-lucy`, `ask-reviewer`, `ask-with-context-reviewer`.
70+
71+## 2. Ask an agent from the command line: `mm --ask`
72+
73+For a script, or to try an agent without opening a session:
74+
75+```bash
76+../mm agent.llmman.yaml --ask lucy -p "In one short sentence: what is the capital of Peru?"
77+# stdout: The capital of Peru is Lima.
78+
79+echo "In one sentence: are goroutines OS threads?" | ../mm agent.llmman.yaml --ask reviewer
80+
81+printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, Philippe.\n---\nWhat is my name and which language do I like? One sentence.\n' \
82+ | ../mm agent.llmman.yaml --ask-with-context jan
83+# stdout: Your name is Philippe, and you like Go.
84+```
85+
86+- The question comes from `-p`, or else from stdin. For `--ask-with-context`, stdin is the context; without `-p`, the context, a `---` line, then the question.
87+- **stdout holds the answer only** (`answer=$(../mm agent.llmman.yaml --ask jan -p "…")` works); the label, the tool calls and errors go to stderr.
88+- Exit codes: `0` answered, `1` failed (reason on stderr, e.g. `[ask: unknown agent "spock" — agents in this config: jan, lucy, reviewer]`), `130` interrupted.
89+- The asked agent's tools are **allowed** in this mode. For an agent that should only answer, use `reviewer`.
90+
91+## 3. Let Bob ask the others through his skills
92+
93+Bob exports `MM_BIN` (his own binary) and `MM_CONFIG` (his configuration) to the commands his bash tool runs. The skills therefore just call `"$MM_BIN" --ask …`, without knowing any path. Ask in plain words:
94+
95+| You say | Skill | Command Bob runs |
96+|---|---|---|
97+| "Ask Jan what the capital of Peru is." | `delegate-to-jan` | `"$MM_BIN" --ask jan -p "What is the capital of Peru?"` |
98+| "I think Go's goroutines are OS threads. Get a second opinion from Lucy on that claim." | `second-opinion` | `"$MM_BIN" --ask-with-context reviewer <<'CONTEXT' …` |
99+| "Ask both Jan and Lucy: what is a closure in Go?" | `compare-agents` | both `--ask` in one command |
100+
101+A real run (`second-opinion`, 97 s in all):
102+
103+```text
104+📖 read_skill: second-opinion
105+🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
106+User: I think Go's goroutines are OS threads.
107+Assistant: Actually, Go goroutines are not OS threads. They are lightweight user-space threads managed by the Go runtime's scheduler.
108+---
109+Is Go's goroutine implementation OS threads?
110+CONTEXT
111+ │ 🗣 asking reviewer (llmman/hf.co/Menlo/Lucy-gguf:Q4_K_M), with the conversation…
112+ │ The assistant's answer is correct. Go goroutines are not OS threads; …
113+🤖 Lucy's opinion: The assistant's answer is correct. … I agree with Lucy.
114+```
115+
116+`compare-agents` has not been tried live yet.
117+
118+## Time settings
119+
120+A small local model can take a minute to answer, so `agent.llmman.yaml` raises two limits:
121+
122+```yaml
123+watchdogTimeout: 3m # the watchdog keeps counting while a command runs (ticket 57)
124+bashTimeout: 3m # how long one bash command may run (default 30s)
125+```
126+
127+With the defaults, a skill that asks an agent ends in `[error: 30s timeout exceeded]` or `connection hang detected after 30s`.
128+
129+## Good to know
130+
131+- **Small models, every tool**: Lucy (1.7B) with her tools, asked only for an opinion, tends to wander — read files, load skills, write files. Hence the tool-less `reviewer` profile for `second-opinion`.
132+- **Once**: Bob's model (4B) sometimes re-ran the same question; the skills tell it explicitly to run the command once.
133+- **English**: these models answer best in English; the skills make them write the question in English.
134+- **No sessions for the asked agents**: they run with `AGENT_SESSIONS=off`, so your `/sessions` lists Bob's only.
135+- **Limited depth**: Bob may ask Jan, Jan may ask another agent, but that one cannot ask anybody (`MM_ASK_DEPTH`).
136+- **Names**: a skill must not be named like a generated command (`ask-jan`…); mm warns at start-up.
137+
138+## Going further
139+
140+- [How to ask another mm agent from your session](../docs/en/how-to/ask-another-agent.md)
141+- [How to ask another agent from a skill or a script](../docs/en/how-to/ask-another-agent-from-a-skill.md)
142+- [Slash commands reference](../docs/en/reference/slash-commands.md#asking-another-agent) · [Command-line reference](../docs/en/reference/cli.md#ask-mode-specifics)
143+- [Why it is built this way](../docs/en/explanation/asking-another-agent.md)
144+- [Run mini-me on llmman](../docs/en/how-to/run-with-llmman.md)
added demo-llmman/README.md +144 -0
new file mode 100644
@@ -0,0 +1,144 @@
1+# Démo mini-me + llmman : interroger d'autres agents
2+
3+Ce dossier montre comment un agent mini-me (Bob) peut poser des questions à d'autres agents mini-me — Jan, Lucy, un relecteur — chacun avec son propre modèle, servi par [llmman](https://github.com/llmmanorg/llmman). English version: [README.en.md](README.en.md).
4+
5+## Ce qu'il y a ici
6+
7+| Fichier | Rôle |
8+|---|---|
9+| `agent.llmman.yaml` | **Bob**, l'agent que vous lancez (`hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M`). Déclare les autres agents sous `mmConfigs` et charge les skills de `skills/`. |
10+| `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), agent complet avec ses outils. |
11+| `agent.lucy.yaml` | **Lucy** (`hf.co/Menlo/Lucy-gguf:Q4_K_M`), agent complet avec ses outils. |
12+| `agent.lucy-reviewer.yaml` | **reviewer** : le modèle de Lucy **sans outils ni skills**, avec un prompt de relecteur. Il ne fait que répondre. |
13+| `skills/delegate-to-jan` | Skill : déléguer une question à Jan. |
14+| `skills/second-opinion` | Skill : demander au relecteur un deuxième avis sur la conversation. |
15+| `skills/compare-agents` | Skill : poser la même question à Jan et à Lucy et comparer. |
16+| `skills/bg-jobs` | Copie de la skill générale (le prompt de Bob y fait référence). |
17+
18+Dans `agent.llmman.yaml` :
19+
20+```yaml
21+mmConfigs:
22+ jan: agent.jan-nano.yaml
23+ lucy: agent.lucy.yaml
24+ reviewer: agent.lucy-reviewer.yaml
25+```
26+
27+## Prérequis
28+
29+1. llmman qui tourne (`llmman serve`, sur `127.0.0.1:17434` ; depuis un Docker Sandbox, mm se replie tout seul sur `host.docker.internal:17434`).
30+2. Les modèles, téléchargés à l'avance (sinon la première question les télécharge et attend) :
31+
32+ ```bash
33+ llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
34+ llmman pull hf.co/Menlo/Jan-nano-gguf:Q4_K_M
35+ llmman pull hf.co/Menlo/Lucy-gguf:Q4_K_M
36+ ```
37+
38+3. `mm` compilé à la racine du dépôt. **Lancez-le depuis ce dossier** : les chemins de `mmConfigs` sont relatifs à `agent.llmman.yaml`, et `skillsDir: skills` au dossier courant.
39+
40+ ```bash
41+ cd demo-llmman
42+ ../mm agent.llmman.yaml # mode ligne
43+ ../mm -tui agent.llmman.yaml # mode fenêtre
44+ ```
45+
46+## 1. Interroger un agent depuis la conversation : `/ask`
47+
48+Dans la session de Bob :
49+
50+```text
51+> /ask-jan In one short sentence: what is the capital of France?
52+🗣 asking jan (llmman/hf.co/Menlo/Jan-nano-gguf:Q4_K_M)…
53+The capital of France is Paris.
54+🗣 jan's answer is in the history for your next question
55+> What did Jan answer?
56+🤖 Jan answered that the capital of France is Paris.
57+```
58+
59+| Commande | Effet |
60+|---|---|
61+| `/ask-<agent> <question>` | Envoie la question seule à l'agent. |
62+| `/ask-with-context-<agent> <question>` | Envoie d'abord une transcription de la conversation (questions, réponses, une ligne par appel d'outil — jamais les sorties d'outils), puis la question. |
63+| `/ask <agent> <question>`, `/ask-with-context <agent> <question>` | Mêmes commandes, l'agent en argument (mode ligne et fenêtre). |
64+
65+- La réponse s'affiche au fil de l'eau, puis reste dans l'historique : c'est le modèle de Bob qui s'en sert à la question suivante.
66+- L'agent appelé démarre au premier `/ask` (comptez quelques secondes de plus) puis reste en vie ; chaque demande ouvre une session neuve chez lui, sans mémoire de la précédente.
67+- `/abort`, `Échap` ou `Ctrl+C` annulent la demande ; rien n'est gardé.
68+- Ses appels d'outils s'affichent sous son nom (`lucy · bash: ls -1 | wc -l`) et ses demandes de permission passent par vous : rien en mode ligne, le réglage `permissions` en mode fenêtre, la boîte de dialogue de l'éditeur en ACP.
69+- Dans un éditeur (Zed…), le menu `/` propose `ask-jan`, `ask-with-context-jan`, `ask-lucy`, `ask-with-context-lucy`, `ask-reviewer`, `ask-with-context-reviewer`.
70+
71+## 2. Interroger un agent en ligne de commande : `mm --ask`
72+
73+Pour un script, ou pour tester un agent sans ouvrir de session :
74+
75+```bash
76+../mm agent.llmman.yaml --ask lucy -p "In one short sentence: what is the capital of Peru?"
77+# stdout : The capital of Peru is Lima.
78+
79+echo "In one sentence: are goroutines OS threads?" | ../mm agent.llmman.yaml --ask reviewer
80+
81+printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, Philippe.\n---\nWhat is my name and which language do I like? One sentence.\n' \
82+ | ../mm agent.llmman.yaml --ask-with-context jan
83+# stdout : Your name is Philippe, and you like Go.
84+```
85+
86+- La question vient de `-p`, sinon de stdin. Pour `--ask-with-context`, stdin contient le contexte ; sans `-p`, le contexte, une ligne `---`, puis la question.
87+- **stdout ne contient que la réponse** (`answer=$(../mm agent.llmman.yaml --ask jan -p "…")` marche) ; l'en-tête, les appels d'outils et les erreurs vont sur stderr.
88+- Codes de sortie : `0` réponse obtenue, `1` échec (raison sur stderr, par exemple `[ask: unknown agent "spock" — agents in this config: jan, lucy, reviewer]`), `130` interrompu.
89+- Les outils de l'agent appelé sont **autorisés** dans ce mode. Pour un agent qui doit seulement répondre, utilisez `reviewer`.
90+
91+## 3. Laisser Bob interroger les autres via ses skills
92+
93+Bob exporte `MM_BIN` (son propre binaire) et `MM_CONFIG` (sa configuration) vers les commandes de son outil bash. Les skills appellent donc simplement `"$MM_BIN" --ask …`, sans connaître aucun chemin. Demandez en langage courant :
94+
95+| Vous dites | Skill | Commande lancée par Bob |
96+|---|---|---|
97+| « Ask Jan what the capital of Peru is. » | `delegate-to-jan` | `"$MM_BIN" --ask jan -p "What is the capital of Peru?"` |
98+| « I think Go's goroutines are OS threads. Get a second opinion from Lucy on that claim. » | `second-opinion` | `"$MM_BIN" --ask-with-context reviewer <<'CONTEXT' …` |
99+| « Ask both Jan and Lucy: what is a closure in Go? » | `compare-agents` | les deux `--ask` dans une seule commande |
100+
101+Exemple réel (`second-opinion`, 97 s au total) :
102+
103+```text
104+📖 read_skill: second-opinion
105+🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
106+User: I think Go's goroutines are OS threads.
107+Assistant: Actually, Go goroutines are not OS threads. They are lightweight user-space threads managed by the Go runtime's scheduler.
108+---
109+Is Go's goroutine implementation OS threads?
110+CONTEXT
111+ │ 🗣 asking reviewer (llmman/hf.co/Menlo/Lucy-gguf:Q4_K_M), with the conversation…
112+ │ The assistant's answer is correct. Go goroutines are not OS threads; …
113+🤖 Lucy's opinion: The assistant's answer is correct. … I agree with Lucy.
114+```
115+
116+`compare-agents` n'a pas encore été testée en conditions réelles.
117+
118+## Réglages de temps
119+
120+Un petit modèle local peut mettre une minute à répondre. `agent.llmman.yaml` relève donc deux délais :
121+
122+```yaml
123+watchdogTimeout: 3m # le watchdog continue de compter pendant qu'une commande tourne (ticket 57)
124+bashTimeout: 3m # durée maximale d'une commande bash (30s par défaut)
125+```
126+
127+Avec les valeurs par défaut, une skill qui interroge un agent finit en `[error: 30s timeout exceeded]` ou `connection hang detected after 30s`.
128+
129+## Bon à savoir
130+
131+- **Petits modèles, tous outils** : Lucy (1,7B) avec ses outils, quand on lui demande seulement un avis, a tendance à s'égarer — lire des fichiers, charger des skills, en écrire. D'où le profil `reviewer` sans outils pour `second-opinion`.
132+- **Une seule fois** : le modèle de Bob (4B) a parfois relancé la même question ; les skills lui disent explicitement de lancer la commande une seule fois.
133+- **Anglais** : ces modèles répondent mieux aux questions en anglais ; les skills leur font écrire la question en anglais.
134+- **Pas de sessions chez les agents appelés** : ils tournent avec `AGENT_SESSIONS=off`, donc vos `/sessions` ne listent que celles de Bob.
135+- **Profondeur limitée** : Bob peut interroger Jan, Jan peut interroger un autre agent, mais ce dernier ne peut plus interroger personne (`MM_ASK_DEPTH`).
136+- **Noms** : une skill ne doit pas porter le nom d'une commande générée (`ask-jan`…) ; mm le signale au démarrage.
137+
138+## Pour aller plus loin
139+
140+- [Comment interroger un autre agent mm depuis votre session](../docs/fr/how-to/ask-another-agent.md)
141+- [Comment interroger un autre agent depuis une skill ou un script](../docs/fr/how-to/ask-another-agent-from-a-skill.md)
142+- [Référence des commandes slash](../docs/fr/reference/slash-commands.md#interroger-un-autre-agent) · [Référence de la ligne de commande](../docs/fr/reference/cli.md#spécificités-du-mode-ask)
143+- [Pourquoi c'est construit ainsi](../docs/fr/explanation/asking-another-agent.md)
144+- [Faire tourner mini-me sur llmman](../docs/fr/how-to/run-with-llmman.md)
new file mode 100644
@@ -0,0 +1,144 @@
1+# Démo mini-me + llmman : interroger d'autres agents
2+
3+Ce dossier montre comment un agent mini-me (Bob) peut poser des questions à d'autres agents mini-me — Jan, Lucy, un relecteur — chacun avec son propre modèle, servi par [llmman](https://github.com/llmmanorg/llmman). English version: [README.en.md](README.en.md).
4+
5+## Ce qu'il y a ici
6+
7+| Fichier | Rôle |
8+|---|---|
9+| `agent.llmman.yaml` | **Bob**, l'agent que vous lancez (`hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M`). Déclare les autres agents sous `mmConfigs` et charge les skills de `skills/`. |
10+| `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), agent complet avec ses outils. |
11+| `agent.lucy.yaml` | **Lucy** (`hf.co/Menlo/Lucy-gguf:Q4_K_M`), agent complet avec ses outils. |
12+| `agent.lucy-reviewer.yaml` | **reviewer** : le modèle de Lucy **sans outils ni skills**, avec un prompt de relecteur. Il ne fait que répondre. |
13+| `skills/delegate-to-jan` | Skill : déléguer une question à Jan. |
14+| `skills/second-opinion` | Skill : demander au relecteur un deuxième avis sur la conversation. |
15+| `skills/compare-agents` | Skill : poser la même question à Jan et à Lucy et comparer. |
16+| `skills/bg-jobs` | Copie de la skill générale (le prompt de Bob y fait référence). |
17+
18+Dans `agent.llmman.yaml` :
19+
20+```yaml
21+mmConfigs:
22+ jan: agent.jan-nano.yaml
23+ lucy: agent.lucy.yaml
24+ reviewer: agent.lucy-reviewer.yaml
25+```
26+
27+## Prérequis
28+
29+1. llmman qui tourne (`llmman serve`, sur `127.0.0.1:17434` ; depuis un Docker Sandbox, mm se replie tout seul sur `host.docker.internal:17434`).
30+2. Les modèles, téléchargés à l'avance (sinon la première question les télécharge et attend) :
31+
32+ ```bash
33+ llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
34+ llmman pull hf.co/Menlo/Jan-nano-gguf:Q4_K_M
35+ llmman pull hf.co/Menlo/Lucy-gguf:Q4_K_M
36+ ```
37+
38+3. `mm` compilé à la racine du dépôt. **Lancez-le depuis ce dossier** : les chemins de `mmConfigs` sont relatifs à `agent.llmman.yaml`, et `skillsDir: skills` au dossier courant.
39+
40+ ```bash
41+ cd demo-llmman
42+ ../mm agent.llmman.yaml # mode ligne
43+ ../mm -tui agent.llmman.yaml # mode fenêtre
44+ ```
45+
46+## 1. Interroger un agent depuis la conversation : `/ask`
47+
48+Dans la session de Bob :
49+
50+```text
51+> /ask-jan In one short sentence: what is the capital of France?
52+🗣 asking jan (llmman/hf.co/Menlo/Jan-nano-gguf:Q4_K_M)…
53+The capital of France is Paris.
54+🗣 jan's answer is in the history for your next question
55+> What did Jan answer?
56+🤖 Jan answered that the capital of France is Paris.
57+```
58+
59+| Commande | Effet |
60+|---|---|
61+| `/ask-<agent> <question>` | Envoie la question seule à l'agent. |
62+| `/ask-with-context-<agent> <question>` | Envoie d'abord une transcription de la conversation (questions, réponses, une ligne par appel d'outil — jamais les sorties d'outils), puis la question. |
63+| `/ask <agent> <question>`, `/ask-with-context <agent> <question>` | Mêmes commandes, l'agent en argument (mode ligne et fenêtre). |
64+
65+- La réponse s'affiche au fil de l'eau, puis reste dans l'historique : c'est le modèle de Bob qui s'en sert à la question suivante.
66+- L'agent appelé démarre au premier `/ask` (comptez quelques secondes de plus) puis reste en vie ; chaque demande ouvre une session neuve chez lui, sans mémoire de la précédente.
67+- `/abort`, `Échap` ou `Ctrl+C` annulent la demande ; rien n'est gardé.
68+- Ses appels d'outils s'affichent sous son nom (`lucy · bash: ls -1 | wc -l`) et ses demandes de permission passent par vous : rien en mode ligne, le réglage `permissions` en mode fenêtre, la boîte de dialogue de l'éditeur en ACP.
69+- Dans un éditeur (Zed…), le menu `/` propose `ask-jan`, `ask-with-context-jan`, `ask-lucy`, `ask-with-context-lucy`, `ask-reviewer`, `ask-with-context-reviewer`.
70+
71+## 2. Interroger un agent en ligne de commande : `mm --ask`
72+
73+Pour un script, ou pour tester un agent sans ouvrir de session :
74+
75+```bash
76+../mm agent.llmman.yaml --ask lucy -p "In one short sentence: what is the capital of Peru?"
77+# stdout : The capital of Peru is Lima.
78+
79+echo "In one sentence: are goroutines OS threads?" | ../mm agent.llmman.yaml --ask reviewer
80+
81+printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, Philippe.\n---\nWhat is my name and which language do I like? One sentence.\n' \
82+ | ../mm agent.llmman.yaml --ask-with-context jan
83+# stdout : Your name is Philippe, and you like Go.
84+```
85+
86+- La question vient de `-p`, sinon de stdin. Pour `--ask-with-context`, stdin contient le contexte ; sans `-p`, le contexte, une ligne `---`, puis la question.
87+- **stdout ne contient que la réponse** (`answer=$(../mm agent.llmman.yaml --ask jan -p "…")` marche) ; l'en-tête, les appels d'outils et les erreurs vont sur stderr.
88+- Codes de sortie : `0` réponse obtenue, `1` échec (raison sur stderr, par exemple `[ask: unknown agent "spock" — agents in this config: jan, lucy, reviewer]`), `130` interrompu.
89+- Les outils de l'agent appelé sont **autorisés** dans ce mode. Pour un agent qui doit seulement répondre, utilisez `reviewer`.
90+
91+## 3. Laisser Bob interroger les autres via ses skills
92+
93+Bob exporte `MM_BIN` (son propre binaire) et `MM_CONFIG` (sa configuration) vers les commandes de son outil bash. Les skills appellent donc simplement `"$MM_BIN" --ask …`, sans connaître aucun chemin. Demandez en langage courant :
94+
95+| Vous dites | Skill | Commande lancée par Bob |
96+|---|---|---|
97+| « Ask Jan what the capital of Peru is. » | `delegate-to-jan` | `"$MM_BIN" --ask jan -p "What is the capital of Peru?"` |
98+| « I think Go's goroutines are OS threads. Get a second opinion from Lucy on that claim. » | `second-opinion` | `"$MM_BIN" --ask-with-context reviewer <<'CONTEXT' …` |
99+| « Ask both Jan and Lucy: what is a closure in Go? » | `compare-agents` | les deux `--ask` dans une seule commande |
100+
101+Exemple réel (`second-opinion`, 97 s au total) :
102+
103+```text
104+📖 read_skill: second-opinion
105+🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
106+User: I think Go's goroutines are OS threads.
107+Assistant: Actually, Go goroutines are not OS threads. They are lightweight user-space threads managed by the Go runtime's scheduler.
108+---
109+Is Go's goroutine implementation OS threads?
110+CONTEXT
111+ │ 🗣 asking reviewer (llmman/hf.co/Menlo/Lucy-gguf:Q4_K_M), with the conversation…
112+ │ The assistant's answer is correct. Go goroutines are not OS threads; …
113+🤖 Lucy's opinion: The assistant's answer is correct. … I agree with Lucy.
114+```
115+
116+`compare-agents` n'a pas encore été testée en conditions réelles.
117+
118+## Réglages de temps
119+
120+Un petit modèle local peut mettre une minute à répondre. `agent.llmman.yaml` relève donc deux délais :
121+
122+```yaml
123+watchdogTimeout: 3m # le watchdog continue de compter pendant qu'une commande tourne (ticket 57)
124+bashTimeout: 3m # durée maximale d'une commande bash (30s par défaut)
125+```
126+
127+Avec les valeurs par défaut, une skill qui interroge un agent finit en `[error: 30s timeout exceeded]` ou `connection hang detected after 30s`.
128+
129+## Bon à savoir
130+
131+- **Petits modèles, tous outils** : Lucy (1,7B) avec ses outils, quand on lui demande seulement un avis, a tendance à s'égarer — lire des fichiers, charger des skills, en écrire. D'où le profil `reviewer` sans outils pour `second-opinion`.
132+- **Une seule fois** : le modèle de Bob (4B) a parfois relancé la même question ; les skills lui disent explicitement de lancer la commande une seule fois.
133+- **Anglais** : ces modèles répondent mieux aux questions en anglais ; les skills leur font écrire la question en anglais.
134+- **Pas de sessions chez les agents appelés** : ils tournent avec `AGENT_SESSIONS=off`, donc vos `/sessions` ne listent que celles de Bob.
135+- **Profondeur limitée** : Bob peut interroger Jan, Jan peut interroger un autre agent, mais ce dernier ne peut plus interroger personne (`MM_ASK_DEPTH`).
136+- **Noms** : une skill ne doit pas porter le nom d'une commande générée (`ask-jan`…) ; mm le signale au démarrage.
137+
138+## Pour aller plus loin
139+
140+- [Comment interroger un autre agent mm depuis votre session](../docs/fr/how-to/ask-another-agent.md)
141+- [Comment interroger un autre agent depuis une skill ou un script](../docs/fr/how-to/ask-another-agent-from-a-skill.md)
142+- [Référence des commandes slash](../docs/fr/reference/slash-commands.md#interroger-un-autre-agent) · [Référence de la ligne de commande](../docs/fr/reference/cli.md#spécificités-du-mode-ask)
143+- [Pourquoi c'est construit ainsi](../docs/fr/explanation/asking-another-agent.md)
144+- [Faire tourner mini-me sur llmman](../docs/fr/how-to/run-with-llmman.md)
modified demo-llmman/agent.llmman.yaml +14 -2
@@ -25,6 +25,10 @@ fallback: http://host.docker.internal:17434/v1
2525 mmConfigs:
2626 jan: agent.jan-nano.yaml
2727 lucy: agent.lucy.yaml
28+ # Lucy's model with no tools and a reviewer prompt: what the second-opinion
29+ # skill asks. A small model with every tool wanders (reads files, loads
30+ # skills) when it is only asked for an opinion.
31+ reviewer: agent.lucy-reviewer.yaml
2832
2933 # Only when the daemon was started with LLMMAN_API_KEYS (mandatory when it
3034 # listens beyond loopback); unset otherwise. The key never goes in this file.
@@ -78,7 +82,9 @@ maxTurns: 40
7882
7983 # Where the `read_skill` tool looks for its markdown procedures.
8084 # The path is relative to the CURRENT DIRECTORY: run the agent from demo-llmman/.
81-skillsDir: ../skills
85+# demo-llmman/skills holds the /ask demo skills (delegate-to-jan, second-opinion,
86+# compare-agents) — they call this same mm back with "$MM_BIN" --ask — and bg-jobs.
87+skillsDir: skills
8288
8389 # Persistent sessions, per project.
8490 # When enabled, every conversation is saved as one JSON file under `dir`.
@@ -177,7 +183,13 @@ sampling:
177183 max_tokens: 4096
178184
179185 # Silence allowed between two tokens once the answer has started.
180-watchdogTimeout: 30s
186+watchdogTimeout: 3m
187+
188+# How long one bash command may run (default 30s). The demo skills call jan and
189+# lucy with "$MM_BIN" --ask, which can take a minute or two with small local
190+# models. The watchdog above keeps counting while a command runs (ticket 57),
191+# so it is raised to the same value.
192+bashTimeout: 3m
181193
182194 # Silence allowed while the model reads the prompt, before the first token:
183195 # on a laptop a 25k-token prompt can take minutes. llama-server reports its
@@ -25,6 +25,10 @@ fallback: http://host.docker.internal:17434/v1
25 mmConfigs:25 mmConfigs:
26 jan: agent.jan-nano.yaml26 jan: agent.jan-nano.yaml
27 lucy: agent.lucy.yaml27 lucy: agent.lucy.yaml
28+ # Lucy's model with no tools and a reviewer prompt: what the second-opinion
29+ # skill asks. A small model with every tool wanders (reads files, loads
30+ # skills) when it is only asked for an opinion.
31+ reviewer: agent.lucy-reviewer.yaml
28 32
29 # Only when the daemon was started with LLMMAN_API_KEYS (mandatory when it33 # Only when the daemon was started with LLMMAN_API_KEYS (mandatory when it
30 # listens beyond loopback); unset otherwise. The key never goes in this file.34 # listens beyond loopback); unset otherwise. The key never goes in this file.
@@ -78,7 +82,9 @@ maxTurns: 40
78 82
79 # Where the `read_skill` tool looks for its markdown procedures. 83 # Where the `read_skill` tool looks for its markdown procedures.
80 # The path is relative to the CURRENT DIRECTORY: run the agent from demo-llmman/.84 # The path is relative to the CURRENT DIRECTORY: run the agent from demo-llmman/.
81-skillsDir: ../skills85+# demo-llmman/skills holds the /ask demo skills (delegate-to-jan, second-opinion,
86+# compare-agents) — they call this same mm back with "$MM_BIN" --ask — and bg-jobs.
87+skillsDir: skills
82 88
83 # Persistent sessions, per project. 89 # Persistent sessions, per project.
84 # When enabled, every conversation is saved as one JSON file under `dir`.90 # When enabled, every conversation is saved as one JSON file under `dir`.
@@ -177,7 +183,13 @@ sampling:
177 max_tokens: 4096183 max_tokens: 4096
178 184
179 # Silence allowed between two tokens once the answer has started.185 # Silence allowed between two tokens once the answer has started.
180-watchdogTimeout: 30s186+watchdogTimeout: 3m
187+
188+# How long one bash command may run (default 30s). The demo skills call jan and
189+# lucy with "$MM_BIN" --ask, which can take a minute or two with small local
190+# models. The watchdog above keeps counting while a command runs (ticket 57),
191+# so it is raised to the same value.
192+bashTimeout: 3m
181 193
182 # Silence allowed while the model reads the prompt, before the first token:194 # Silence allowed while the model reads the prompt, before the first token:
183 # on a laptop a 25k-token prompt can take minutes. llama-server reports its195 # on a laptop a 25k-token prompt can take minutes. llama-server reports its
added demo-llmman/agent.lucy-reviewer.yaml +140 -0
new file mode 100644
@@ -0,0 +1,140 @@
1+# Agent settings — Lucy as a REVIEWER (no tools), for the second-opinion skill: mini-me on hf.co/Menlo/Lucy-gguf:Q4_K_M, served by llmman (https://github.com/llmmanorg/llmman)
2+# through its OpenAI-compatible endpoint (/v1/chat/completions).
3+#
4+# llmman pull hf.co/Menlo/Lucy-gguf:Q4_K_M # optional: the first question pulls it otherwise
5+# llmman serve # 127.0.0.1:17434
6+#
7+# llmman loads the model on the first request (in its own llama-server) and
8+# unloads it after five idle minutes.
9+provider: llmman
10+
11+# The model id exactly as `llmman list` / GET /v1/models spells it.
12+model: hf.co/Menlo/Lucy-gguf:Q4_K_M
13+
14+# `llmman serve`'s default bind; the OpenAI routes sit under /v1.
15+baseUrl: http://127.0.0.1:17434/v1
16+
17+# From a Docker sandbox, the daemon runs on the host.
18+fallback: http://host.docker.internal:17434/v1
19+
20+# Only when the daemon was started with LLMMAN_API_KEYS (mandatory when it
21+# listens beyond loopback); unset otherwise. The key never goes in this file.
22+# apiKeyEnv: LLMMAN_API_KEY
23+
24+# The `bash` tool.
25+# `false` removes it: the model can still read and edit files (editTools) and load skills,
26+# but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool".
27+bashTool: false
28+
29+# The built-in file tools: read_file, write_file, edit_file.
30+editTools: false
31+
32+# The built-in plan tool for managing multi-step plans.
33+# Set to false to disable plan creation and tracking via the `plan` tool.
34+planTool: false
35+
36+# Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
37+# The file extension picks the server. A server missing from the PATH is skipped with a warning.
38+# No server listed = no `lsp` tool. Examples (uncomment what you have installed):
39+# lsp:
40+# servers:
41+# - name: gopls
42+# command: gopls
43+# extensions: [".go"]
44+# - name: typescript
45+# command: typescript-language-server
46+# args: ["--stdio"]
47+# extensions: [".ts", ".tsx", ".js", ".jsx"]
48+# - name: python
49+# command: pyright-langserver
50+# args: ["--stdio"]
51+# extensions: [".py"]
52+# - name: rust
53+# command: rust-analyzer
54+# extensions: [".rs"]
55+
56+# How many tokens the server actually serves.
57+# 0 = ask llmman: /api/ps tells it once the model is loaded, so the window is
58+# learned before the second question. Set it here if you want it from the start
59+# — and to the same value as LLMMAN_CONTEXT_LENGTH if you set that variable.
60+contextWindow: 0
61+
62+# Max number of characters a tool returns to the model.
63+# Context safeguard: beyond that, the output is truncated (beginning + end kept).
64+maxOutput: 16000
65+
66+# Max number of model <-> tools round trips for a single question.
67+# Raise it for tasks that need long chains of commands.
68+maxTurns: 40
69+
70+# Where the `read_skill` tool looks for its markdown procedures.
71+# The path is relative to the CURRENT DIRECTORY: run the agent from demo-llmman/.
72+# No skills either: a directory that does not exist declares no read_skill tool.
73+skillsDir: no-skills-for-the-reviewer
74+
75+# Persistent sessions, per project.
76+# When enabled, every conversation is saved as one JSON file under `dir`.
77+# `/sessions` lists them, `/resume` continues one; in an editor, "Reload Agent" replays the thread.
78+sessions:
79+ enabled: true
80+ dir: .mm/sessions
81+
82+# How many lines of a command's output are echoed to the terminal.
83+# 0 disables it.
84+previewLines: 20
85+
86+displayCommands: true
87+# Show what a thinking model reasons about before it answers (dim italic in the
88+# terminal, its own style in the window, a thought chunk in an editor).
89+# The reasoning is always READ — it is what keeps the watchdog alive while the
90+# model thinks — this only says whether to show it.
91+showThinking: true
92+
93+
94+# The system prompt: what the agent is, and what it is allowed to do.
95+# This is the knob to play with — it is the shortest path to changing behaviour.
96+system: |
97+ Your name is Lucy. You are a reviewer: another assistant sends you a
98+ conversation and a question about it. Answer in English, in at most five
99+ sentences: say whether the assistant's last answer is right, and correct it
100+ when it is not. You have no tools: answer from what you are given and from
101+ what you know, and say so when you are not sure.
102+
103+# Generation settings (OpenAI API keys). Low temperature for a coding agent:
104+# we want precise and reproducible answers, not creativity.
105+sampling:
106+ temperature: 0.0
107+ parallel_tool_calls: false
108+ top_p: 0.9
109+ max_tokens: 4096
110+
111+# Silence allowed between two tokens once the answer has started.
112+watchdogTimeout: 30s
113+
114+# Silence allowed while the model reads the prompt, before the first token:
115+# on a laptop a 25k-token prompt can take minutes. llama-server reports its
116+# progress meanwhile, through llmman ("Reading the prompt 12.0k/25.3k tokens"), and each report
117+# restarts this clock, so it only has to cover one batch of the prompt.
118+firstTokenTimeout: 3m
119+
120+# Context compression.
121+# ON here: the agent learns the window from /api/ps, so `threshold` has a number to apply to.
122+context:
123+ enabled: true
124+
125+ # Compress when the history reaches this share of the window.
126+ threshold: 75
127+
128+ # Fallback on the message count, for when the window is unknown.
129+ # One command costs 2 messages (call + response), so 80 is roughly 30 commands of history.
130+ maxMessages: 80
131+
132+ # How many recent questions (with their commands and answers) are kept as-is instead of being summarised.
133+ # The model works on the last few turns, so summarising them would make it redo what it just did.
134+ keepLastTurns: 3
135+
136+ # max_tokens of the summary request. Reasoning is turned off for it on this provider
137+ # (a thinking model would otherwise spend the budget thinking and write nothing).
138+ summaryMaxTokens: 1200
139+
140+ showStats: true
new file mode 100644
@@ -0,0 +1,140 @@
1+# Agent settings — Lucy as a REVIEWER (no tools), for the second-opinion skill: mini-me on hf.co/Menlo/Lucy-gguf:Q4_K_M, served by llmman (https://github.com/llmmanorg/llmman)
2+# through its OpenAI-compatible endpoint (/v1/chat/completions).
3+#
4+# llmman pull hf.co/Menlo/Lucy-gguf:Q4_K_M # optional: the first question pulls it otherwise
5+# llmman serve # 127.0.0.1:17434
6+#
7+# llmman loads the model on the first request (in its own llama-server) and
8+# unloads it after five idle minutes.
9+provider: llmman
10+
11+# The model id exactly as `llmman list` / GET /v1/models spells it.
12+model: hf.co/Menlo/Lucy-gguf:Q4_K_M
13+
14+# `llmman serve`'s default bind; the OpenAI routes sit under /v1.
15+baseUrl: http://127.0.0.1:17434/v1
16+
17+# From a Docker sandbox, the daemon runs on the host.
18+fallback: http://host.docker.internal:17434/v1
19+
20+# Only when the daemon was started with LLMMAN_API_KEYS (mandatory when it
21+# listens beyond loopback); unset otherwise. The key never goes in this file.
22+# apiKeyEnv: LLMMAN_API_KEY
23+
24+# The `bash` tool.
25+# `false` removes it: the model can still read and edit files (editTools) and load skills,
26+# but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool".
27+bashTool: false
28+
29+# The built-in file tools: read_file, write_file, edit_file.
30+editTools: false
31+
32+# The built-in plan tool for managing multi-step plans.
33+# Set to false to disable plan creation and tracking via the `plan` tool.
34+planTool: false
35+
36+# Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
37+# The file extension picks the server. A server missing from the PATH is skipped with a warning.
38+# No server listed = no `lsp` tool. Examples (uncomment what you have installed):
39+# lsp:
40+# servers:
41+# - name: gopls
42+# command: gopls
43+# extensions: [".go"]
44+# - name: typescript
45+# command: typescript-language-server
46+# args: ["--stdio"]
47+# extensions: [".ts", ".tsx", ".js", ".jsx"]
48+# - name: python
49+# command: pyright-langserver
50+# args: ["--stdio"]
51+# extensions: [".py"]
52+# - name: rust
53+# command: rust-analyzer
54+# extensions: [".rs"]
55+
56+# How many tokens the server actually serves.
57+# 0 = ask llmman: /api/ps tells it once the model is loaded, so the window is
58+# learned before the second question. Set it here if you want it from the start
59+# — and to the same value as LLMMAN_CONTEXT_LENGTH if you set that variable.
60+contextWindow: 0
61+
62+# Max number of characters a tool returns to the model.
63+# Context safeguard: beyond that, the output is truncated (beginning + end kept).
64+maxOutput: 16000
65+
66+# Max number of model <-> tools round trips for a single question.
67+# Raise it for tasks that need long chains of commands.
68+maxTurns: 40
69+
70+# Where the `read_skill` tool looks for its markdown procedures.
71+# The path is relative to the CURRENT DIRECTORY: run the agent from demo-llmman/.
72+# No skills either: a directory that does not exist declares no read_skill tool.
73+skillsDir: no-skills-for-the-reviewer
74+
75+# Persistent sessions, per project.
76+# When enabled, every conversation is saved as one JSON file under `dir`.
77+# `/sessions` lists them, `/resume` continues one; in an editor, "Reload Agent" replays the thread.
78+sessions:
79+ enabled: true
80+ dir: .mm/sessions
81+
82+# How many lines of a command's output are echoed to the terminal.
83+# 0 disables it.
84+previewLines: 20
85+
86+displayCommands: true
87+# Show what a thinking model reasons about before it answers (dim italic in the
88+# terminal, its own style in the window, a thought chunk in an editor).
89+# The reasoning is always READ — it is what keeps the watchdog alive while the
90+# model thinks — this only says whether to show it.
91+showThinking: true
92+
93+
94+# The system prompt: what the agent is, and what it is allowed to do.
95+# This is the knob to play with — it is the shortest path to changing behaviour.
96+system: |
97+ Your name is Lucy. You are a reviewer: another assistant sends you a
98+ conversation and a question about it. Answer in English, in at most five
99+ sentences: say whether the assistant's last answer is right, and correct it
100+ when it is not. You have no tools: answer from what you are given and from
101+ what you know, and say so when you are not sure.
102+
103+# Generation settings (OpenAI API keys). Low temperature for a coding agent:
104+# we want precise and reproducible answers, not creativity.
105+sampling:
106+ temperature: 0.0
107+ parallel_tool_calls: false
108+ top_p: 0.9
109+ max_tokens: 4096
110+
111+# Silence allowed between two tokens once the answer has started.
112+watchdogTimeout: 30s
113+
114+# Silence allowed while the model reads the prompt, before the first token:
115+# on a laptop a 25k-token prompt can take minutes. llama-server reports its
116+# progress meanwhile, through llmman ("Reading the prompt 12.0k/25.3k tokens"), and each report
117+# restarts this clock, so it only has to cover one batch of the prompt.
118+firstTokenTimeout: 3m
119+
120+# Context compression.
121+# ON here: the agent learns the window from /api/ps, so `threshold` has a number to apply to.
122+context:
123+ enabled: true
124+
125+ # Compress when the history reaches this share of the window.
126+ threshold: 75
127+
128+ # Fallback on the message count, for when the window is unknown.
129+ # One command costs 2 messages (call + response), so 80 is roughly 30 commands of history.
130+ maxMessages: 80
131+
132+ # How many recent questions (with their commands and answers) are kept as-is instead of being summarised.
133+ # The model works on the last few turns, so summarising them would make it redo what it just did.
134+ keepLastTurns: 3
135+
136+ # max_tokens of the summary request. Reasoning is turned off for it on this provider
137+ # (a thinking model would otherwise spend the budget thinking and write nothing).
138+ summaryMaxTokens: 1200
139+
140+ showStats: true
added demo-llmman/skills/bg-jobs/SKILL.md +82 -0
new file mode 100644
@@ -0,0 +1,82 @@
1+---
2+name: bg-jobs
3+description: Start, wait on, inspect and stop a long-running background job — a server, a watcher, a build that does not end — without blocking the turn.
4+keywords: background, job, server, serve, start, daemon, nohup, watch, tail, log, pid, kill, stop, port, running
5+---
6+
7+# Background jobs
8+
9+## When to use it
10+
11+Any command that **serves, watches, or runs until stopped**: an HTTP server, `npm run dev`, `go run` on a web app, a file watcher, a queue worker. Read this before you start one, and before you wait on, inspect or stop one.
12+
13+A command that never returns will hold the turn for its whole budget and then be killed. The answer the user asked for never arrives.
14+
15+## Start it
16+
17+Both streams redirected, the pid kept:
18+
19+```bash
20+nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid
21+```
22+
23+Three parts, and all three matter:
24+
25+- **`> /tmp/<job>.log 2>&1`** — both streams. Redirecting only stdout is the classic mistake: the job keeps the tool's own error pipe open, and the call cannot finish until the job does. You get the 30-second budget spent on nothing.
26+- **`&`** — the shell returns at once.
27+- **`echo $! > /tmp/<job>.pid`** — without the pid you cannot stop it later except by guessing at `pkill` patterns, which also kills things you did not start.
28+
29+Use one name per job, and reuse it for the log and the pid file.
30+
31+## Wait for it to be ready
32+
33+**Do not `sleep` and hope.** Poll for the thing you actually need, with a bound:
34+
35+```bash
36+for i in $(seq 1 30); do
37+ curl -fsS localhost:8080/ >/dev/null 2>&1 && echo ready && break
38+ sleep 0.5
39+done
40+```
41+
42+For something that is not an HTTP server, poll the log for the line it prints when it is up:
43+
44+```bash
45+for i in $(seq 1 30); do
46+ grep -q "listening on" /tmp/<job>.log && echo ready && break
47+ sleep 0.5
48+done
49+```
50+
51+If the loop ends without `ready`, the job did not start — go read the log.
52+
53+## Inspect it
54+
55+```bash
56+tail -n 40 /tmp/<job>.log # what it said
57+kill -0 "$(cat /tmp/<job>.pid)" 2>/dev/null && echo alive || echo dead
58+```
59+
60+`kill -0` sends no signal; it only asks whether the process is there. A job that is `dead` right after starting almost always says why in the last lines of its log — read them before changing anything.
61+
62+## Stop it
63+
64+```bash
65+kill "$(cat /tmp/<job>.pid)" 2>/dev/null
66+sleep 0.5
67+kill -0 "$(cat /tmp/<job>.pid)" 2>/dev/null && kill -9 "$(cat /tmp/<job>.pid)"
68+rm -f /tmp/<job>.pid
69+```
70+
71+Ask politely first (`kill` sends TERM, which lets a server close its listener), then insist. Removing the pid file afterwards stops a later command from acting on a pid the system has since reused for something else.
72+
73+## Before you finish
74+
75+**Stop every job you started**, and **say which ones you left running** and why. A server still holding port 8080 after the turn is a surprise the user will hit later, when nothing on screen explains it.
76+
77+## Traps
78+
79+- **One shell per call.** Each `bash` call is a new shell in the same directory: a job started in one call is still running in the next, but shell variables are gone. That is exactly why the pid goes to a file and not to `$PID`.
80+- **The port is already in use.** A previous run left something behind. `lsof -i :8080` or `ss -lptn 'sport = :8080'` names the holder; stop that before starting again, rather than moving to another port and forgetting.
81+- **Nothing is interactive.** There is no stdin. A command that asks a question will hang until it is killed; pass the answer as a flag, or do not run it.
82+- **`nohup` writes `nohup.out`** in the working directory when you forget to redirect — if you see that file appear, a redirect is missing.
new file mode 100644
@@ -0,0 +1,82 @@
1+---
2+name: bg-jobs
3+description: Start, wait on, inspect and stop a long-running background job — a server, a watcher, a build that does not end — without blocking the turn.
4+keywords: background, job, server, serve, start, daemon, nohup, watch, tail, log, pid, kill, stop, port, running
5+---
6+
7+# Background jobs
8+
9+## When to use it
10+
11+Any command that **serves, watches, or runs until stopped**: an HTTP server, `npm run dev`, `go run` on a web app, a file watcher, a queue worker. Read this before you start one, and before you wait on, inspect or stop one.
12+
13+A command that never returns will hold the turn for its whole budget and then be killed. The answer the user asked for never arrives.
14+
15+## Start it
16+
17+Both streams redirected, the pid kept:
18+
19+```bash
20+nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid
21+```
22+
23+Three parts, and all three matter:
24+
25+- **`> /tmp/<job>.log 2>&1`** — both streams. Redirecting only stdout is the classic mistake: the job keeps the tool's own error pipe open, and the call cannot finish until the job does. You get the 30-second budget spent on nothing.
26+- **`&`** — the shell returns at once.
27+- **`echo $! > /tmp/<job>.pid`** — without the pid you cannot stop it later except by guessing at `pkill` patterns, which also kills things you did not start.
28+
29+Use one name per job, and reuse it for the log and the pid file.
30+
31+## Wait for it to be ready
32+
33+**Do not `sleep` and hope.** Poll for the thing you actually need, with a bound:
34+
35+```bash
36+for i in $(seq 1 30); do
37+ curl -fsS localhost:8080/ >/dev/null 2>&1 && echo ready && break
38+ sleep 0.5
39+done
40+```
41+
42+For something that is not an HTTP server, poll the log for the line it prints when it is up:
43+
44+```bash
45+for i in $(seq 1 30); do
46+ grep -q "listening on" /tmp/<job>.log && echo ready && break
47+ sleep 0.5
48+done
49+```
50+
51+If the loop ends without `ready`, the job did not start — go read the log.
52+
53+## Inspect it
54+
55+```bash
56+tail -n 40 /tmp/<job>.log # what it said
57+kill -0 "$(cat /tmp/<job>.pid)" 2>/dev/null && echo alive || echo dead
58+```
59+
60+`kill -0` sends no signal; it only asks whether the process is there. A job that is `dead` right after starting almost always says why in the last lines of its log — read them before changing anything.
61+
62+## Stop it
63+
64+```bash
65+kill "$(cat /tmp/<job>.pid)" 2>/dev/null
66+sleep 0.5
67+kill -0 "$(cat /tmp/<job>.pid)" 2>/dev/null && kill -9 "$(cat /tmp/<job>.pid)"
68+rm -f /tmp/<job>.pid
69+```
70+
71+Ask politely first (`kill` sends TERM, which lets a server close its listener), then insist. Removing the pid file afterwards stops a later command from acting on a pid the system has since reused for something else.
72+
73+## Before you finish
74+
75+**Stop every job you started**, and **say which ones you left running** and why. A server still holding port 8080 after the turn is a surprise the user will hit later, when nothing on screen explains it.
76+
77+## Traps
78+
79+- **One shell per call.** Each `bash` call is a new shell in the same directory: a job started in one call is still running in the next, but shell variables are gone. That is exactly why the pid goes to a file and not to `$PID`.
80+- **The port is already in use.** A previous run left something behind. `lsof -i :8080` or `ss -lptn 'sport = :8080'` names the holder; stop that before starting again, rather than moving to another port and forgetting.
81+- **Nothing is interactive.** There is no stdin. A command that asks a question will hang until it is killed; pass the answer as a flag, or do not run it.
82+- **`nohup` writes `nohup.out`** in the working directory when you forget to redirect — if you see that file appear, a redirect is missing.
added demo-llmman/skills/compare-agents/SKILL.md +23 -0
new file mode 100644
@@ -0,0 +1,23 @@
1+---
2+name: compare-agents
3+description: Ask the same question to Jan and to Lucy, two other agents, and compare their answers.
4+keywords: compare, both agents, jan and lucy, ask both, compare les, les deux, comparer
5+---
6+
7+# Compare Jan and Lucy
8+
9+## When to use it
10+
11+The user wants to compare agents, or asks you to ask both Jan and Lucy.
12+
13+## What to do
14+
15+Write the question in English, as one self-contained sentence (neither agent sees this conversation). Then call your `bash` tool ONCE with both commands, so the two answers come back together:
16+
17+ echo "## Jan"; "$MM_BIN" --ask jan -p "<question>"; echo; echo "## Lucy"; "$MM_BIN" --ask lucy -p "<question>"
18+
19+- Each command prints the agent's answer, and only that. The pair can take a few minutes.
20+- Run it ONCE. Whatever it prints are the answers: do not run it again.
21+- A line starting with `[ask:` means that agent could not answer: say so for that agent.
22+
23+Then answer the user with a short comparison: what Jan said, what Lucy said, where they agree, where they differ. Do not add an answer of your own unless the user asks for it.
new file mode 100644
@@ -0,0 +1,23 @@
1+---
2+name: compare-agents
3+description: Ask the same question to Jan and to Lucy, two other agents, and compare their answers.
4+keywords: compare, both agents, jan and lucy, ask both, compare les, les deux, comparer
5+---
6+
7+# Compare Jan and Lucy
8+
9+## When to use it
10+
11+The user wants to compare agents, or asks you to ask both Jan and Lucy.
12+
13+## What to do
14+
15+Write the question in English, as one self-contained sentence (neither agent sees this conversation). Then call your `bash` tool ONCE with both commands, so the two answers come back together:
16+
17+ echo "## Jan"; "$MM_BIN" --ask jan -p "<question>"; echo; echo "## Lucy"; "$MM_BIN" --ask lucy -p "<question>"
18+
19+- Each command prints the agent's answer, and only that. The pair can take a few minutes.
20+- Run it ONCE. Whatever it prints are the answers: do not run it again.
21+- A line starting with `[ask:` means that agent could not answer: say so for that agent.
22+
23+Then answer the user with a short comparison: what Jan said, what Lucy said, where they agree, where they differ. Do not add an answer of your own unless the user asks for it.
added demo-llmman/skills/delegate-to-jan/SKILL.md +23 -0
new file mode 100644
@@ -0,0 +1,23 @@
1+---
2+name: delegate-to-jan
3+description: Delegate a research or fact question to Jan, another agent (Jan-nano 4B), and report Jan's answer.
4+keywords: jan, delegate, research, look up, other agent, demande à jan, cherche, recherche
5+---
6+
7+# Delegate to Jan
8+
9+## When to use it
10+
11+The user asks you to ask Jan, or to delegate a question to another agent, or wants a fact you cannot check with a command.
12+
13+## What to do
14+
15+Call your `bash` tool with this command, replacing `<question>` with the user's question, in English, as one self-contained sentence (Jan does not see this conversation):
16+
17+ "$MM_BIN" --ask jan -p "<question>"
18+
19+- The command prints Jan's answer, and only that. It can take a minute.
20+- Run it ONCE. Whatever it prints is the answer: do not run it again, do not rephrase and ask again.
21+- If it prints `[ask: …]` instead, Jan could not answer: tell the user that line, do not answer in Jan's place.
22+
23+Then answer the user in a few lines, starting with "Jan says:" — Jan's answer, not yours.
new file mode 100644
@@ -0,0 +1,23 @@
1+---
2+name: delegate-to-jan
3+description: Delegate a research or fact question to Jan, another agent (Jan-nano 4B), and report Jan's answer.
4+keywords: jan, delegate, research, look up, other agent, demande à jan, cherche, recherche
5+---
6+
7+# Delegate to Jan
8+
9+## When to use it
10+
11+The user asks you to ask Jan, or to delegate a question to another agent, or wants a fact you cannot check with a command.
12+
13+## What to do
14+
15+Call your `bash` tool with this command, replacing `<question>` with the user's question, in English, as one self-contained sentence (Jan does not see this conversation):
16+
17+ "$MM_BIN" --ask jan -p "<question>"
18+
19+- The command prints Jan's answer, and only that. It can take a minute.
20+- Run it ONCE. Whatever it prints is the answer: do not run it again, do not rephrase and ask again.
21+- If it prints `[ask: …]` instead, Jan could not answer: tell the user that line, do not answer in Jan's place.
22+
23+Then answer the user in a few lines, starting with "Jan says:" — Jan's answer, not yours.
added demo-llmman/skills/second-opinion/SKILL.md +28 -0
new file mode 100644
@@ -0,0 +1,28 @@
1+---
2+name: second-opinion
3+description: Get a second opinion from Lucy, a reviewer agent (Lucy 1.7B, no tools), on the conversation so far.
4+keywords: second opinion, lucy, review, double-check, avis, deuxième avis, vérifie, relis
5+---
6+
7+# Second opinion from Lucy
8+
9+## When to use it
10+
11+The user wants a second opinion, a review of your last answer, or asks what Lucy thinks.
12+
13+## What to do
14+
15+Lucy does not see this conversation: you send it to her. Call your `bash` tool with this command, replacing the lines between `CONTEXT` and `---` with a short summary of the conversation — one `User:` or `Assistant:` line per message, the last answer included — and `<question>` with what to check, in English:
16+
17+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
18+ User: <first question>
19+ Assistant: <your answer>
20+ ---
21+ <question>
22+ CONTEXT
23+
24+- The command prints Lucy's answer, and only that. It can take a minute.
25+- Run it ONCE. Whatever it prints is Lucy's opinion: do not run it again, do not rephrase the question and ask again, even if you disagree with her.
26+- If it prints `[ask: …]` instead, tell the user that Lucy could not answer, with that line.
27+
28+Then answer the user: "Lucy's opinion:" followed by her answer, then one sentence saying whether you agree and why.
new file mode 100644
@@ -0,0 +1,28 @@
1+---
2+name: second-opinion
3+description: Get a second opinion from Lucy, a reviewer agent (Lucy 1.7B, no tools), on the conversation so far.
4+keywords: second opinion, lucy, review, double-check, avis, deuxième avis, vérifie, relis
5+---
6+
7+# Second opinion from Lucy
8+
9+## When to use it
10+
11+The user wants a second opinion, a review of your last answer, or asks what Lucy thinks.
12+
13+## What to do
14+
15+Lucy does not see this conversation: you send it to her. Call your `bash` tool with this command, replacing the lines between `CONTEXT` and `---` with a short summary of the conversation — one `User:` or `Assistant:` line per message, the last answer included — and `<question>` with what to check, in English:
16+
17+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
18+ User: <first question>
19+ Assistant: <your answer>
20+ ---
21+ <question>
22+ CONTEXT
23+
24+- The command prints Lucy's answer, and only that. It can take a minute.
25+- Run it ONCE. Whatever it prints is Lucy's opinion: do not run it again, do not rephrase the question and ask again, even if you disagree with her.
26+- If it prints `[ask: …]` instead, tell the user that Lucy could not answer, with that line.
27+
28+Then answer the user: "Lucy's opinion:" followed by her answer, then one sentence saying whether you agree and why.
added demo/newfile.txt +0 -0
new file mode 100644
new file mode 100644
modified docs/en/README.md +1 -0
@@ -14,6 +14,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr)
1414 - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demo-llmman/`
1515 - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demo-ollama/`
1616 - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>`
17+- [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, the demo skills
1718 - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md)
1819 - [Run mini-me in a Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — the template and the two kits under `docker-sbx/`, window mode and editor
1920 - [Run mini-me with the v3 sandbox kits](how-to/run-with-sandbox-kits-v3.md) — the same two kits for the kit specification v3, under `docker-sbx-v3/`
@@ -14,6 +14,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr)
14 - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demo-llmman/`14 - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demo-llmman/`
15 - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demo-ollama/`15 - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demo-ollama/`
16 - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>`16 - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>`
17+- [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, the demo skills
17 - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md)18 - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md)
18 - [Run mini-me in a Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — the template and the two kits under `docker-sbx/`, window mode and editor19 - [Run mini-me in a Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — the template and the two kits under `docker-sbx/`, window mode and editor
19 - [Run mini-me with the v3 sandbox kits](how-to/run-with-sandbox-kits-v3.md) — the same two kits for the kit specification v3, under `docker-sbx-v3/`20 - [Run mini-me with the v3 sandbox kits](how-to/run-with-sandbox-kits-v3.md) — the same two kits for the kit specification v3, under `docker-sbx-v3/`
modified docs/en/explanation/asking-another-agent.md +4 -0
@@ -22,6 +22,10 @@
2222
2323 **Chains stop.** `/ask` is a command a person types; no model can call it, so no agent loops on its own. A person could still type `/ask` into an agent that was itself asked, so each child is told its depth (`MM_ASK_DEPTH`): your agent may ask bob, bob may ask riker, riker may not ask anyone.
2424
25+**The same machinery answers scripts.** `mm --ask <agent>` and `mm --ask-with-context <agent>` are the slash commands for whatever can only run a command — a skill, a script. They start the same child over ACP, with the same environment fence and depth limit, but the answer goes to stdout and everything else to stderr, so a caller captures the answer alone. mm exports `MM_BIN` and `MM_CONFIG` to the commands its `bash` tool runs, so a skill calls "this mm, with this configuration" without knowing either path. Two things differ from the slash commands, both because the caller is a command and not a person. The agent's tool calls are allowed, as in the line mode: the person already approved the `mm --ask` command itself. And `mm --ask` watches its parent process: the `bash` tool kills `bash` when its budget runs out, not bash's children, and an orphaned ask kept its agent running — and writing files — after nobody waited for it any more.
26+
27+**Time is the weak point.** A local model can take a minute to answer, and one `bash` command had 30 seconds; `bashTimeout` now sets that budget. The watchdog of the asking agent also keeps counting while a command runs (ticket 57), so it has to be raised too until that ticket is fixed.
28+
2529 ## Rejected alternatives
2630
2731 - **`mm -p "<prompt>"` as a subprocess.** One process per question and nothing to keep alive — but its output is a terminal screen (answer, reasoning, tool echoes, colours, the command recap) that would have to be parsed; it never asks permission, so bob would run commands unchecked; it cannot be cancelled except by killing it.
@@ -22,6 +22,10 @@
22 22
23 **Chains stop.** `/ask` is a command a person types; no model can call it, so no agent loops on its own. A person could still type `/ask` into an agent that was itself asked, so each child is told its depth (`MM_ASK_DEPTH`): your agent may ask bob, bob may ask riker, riker may not ask anyone.23 **Chains stop.** `/ask` is a command a person types; no model can call it, so no agent loops on its own. A person could still type `/ask` into an agent that was itself asked, so each child is told its depth (`MM_ASK_DEPTH`): your agent may ask bob, bob may ask riker, riker may not ask anyone.
24 24
25+**The same machinery answers scripts.** `mm --ask <agent>` and `mm --ask-with-context <agent>` are the slash commands for whatever can only run a command — a skill, a script. They start the same child over ACP, with the same environment fence and depth limit, but the answer goes to stdout and everything else to stderr, so a caller captures the answer alone. mm exports `MM_BIN` and `MM_CONFIG` to the commands its `bash` tool runs, so a skill calls "this mm, with this configuration" without knowing either path. Two things differ from the slash commands, both because the caller is a command and not a person. The agent's tool calls are allowed, as in the line mode: the person already approved the `mm --ask` command itself. And `mm --ask` watches its parent process: the `bash` tool kills `bash` when its budget runs out, not bash's children, and an orphaned ask kept its agent running — and writing files — after nobody waited for it any more.
26+
27+**Time is the weak point.** A local model can take a minute to answer, and one `bash` command had 30 seconds; `bashTimeout` now sets that budget. The watchdog of the asking agent also keeps counting while a command runs (ticket 57), so it has to be raised too until that ticket is fixed.
28+
25 ## Rejected alternatives29 ## Rejected alternatives
26 30
27 - **`mm -p "<prompt>"` as a subprocess.** One process per question and nothing to keep alive — but its output is a terminal screen (answer, reasoning, tool echoes, colours, the command recap) that would have to be parsed; it never asks permission, so bob would run commands unchecked; it cannot be cancelled except by killing it.31 - **`mm -p "<prompt>"` as a subprocess.** One process per question and nothing to keep alive — but its output is a terminal screen (answer, reasoning, tool echoes, colours, the command recap) that would have to be parsed; it never asks permission, so bob would run commands unchecked; it cannot be cancelled except by killing it.
added docs/en/how-to/ask-another-agent-from-a-skill.md +61 -0
new file mode 100644
@@ -0,0 +1,61 @@
1+# How to ask another agent from a skill or a script
2+
3+This guide shows how to make a skill — or any shell script — put a question to another mm agent and use its answer. It assumes the agents are already declared under `mmConfigs` (see [how to ask another agent](ask-another-agent.md)).
4+
5+## Steps
6+
7+1. In the skill, call the same mm back through the variables mm exports to the commands its `bash` tool runs. `MM_BIN` is the running `mm`, and `MM_CONFIG` its configuration, so `--ask` finds the agent names without a path:
8+
9+ ```bash
10+ "$MM_BIN" --ask jan -p "What is the capital of Peru?"
11+ ```
12+
13+ stdout carries the answer and nothing else; the label and the agent's tool calls go to stderr.
14+
15+2. To send the conversation along, write it on stdin, then a `---` line, then the question:
16+
17+ ```bash
18+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
19+ User: I think Go's goroutines are OS threads.
20+ Assistant: They are not: the Go runtime schedules them onto a few OS threads.
21+ ---
22+ Is the assistant right?
23+ CONTEXT
24+ ```
25+
26+ The model writes this summary itself: a command cannot read the conversation.
27+
28+3. Tell the model, in the skill, to run the command once and to report the answer as the other agent's. `demo-llmman/skills/` has three complete examples:
29+
30+ - `delegate-to-jan` — a question delegated to Jan;
31+ - `second-opinion` — the conversation sent to a reviewer;
32+ - `compare-agents` — the same question to Jan and Lucy, in one command.
33+
34+4. Raise the time limits in the asking agent's configuration. A local model can take a minute to answer, and both limits default to well under that:
35+
36+ ```yaml
37+ bashTimeout: 3m # one bash command (default 30s)
38+ watchdogTimeout: 3m # keeps counting while a command runs (ticket 57)
39+ ```
40+
41+5. Run the asking agent from the directory the skills expect and ask in plain words:
42+
43+ ```text
44+ > I think Go's goroutines are OS threads. Get a second opinion from Lucy on that claim.
45+ 📖 read_skill: second-opinion
46+ 🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT' …
47+ │ 🗣 asking reviewer (llmman/hf.co/Menlo/Lucy-gguf:Q4_K_M), with the conversation…
48+ │ The assistant's answer is correct. Go goroutines are not OS threads; …
49+ ```
50+
51+## Variants
52+
53+- **From a terminal or a script, outside mm.** Name the configuration: `mm -config demo-llmman/agent.llmman.yaml --ask lucy -p "…"`, or put the question on stdin. Check the exit code: 0 answered, 1 failed (the reason is on stderr), 130 interrupted.
54+- **An agent that should only answer.** Give it a configuration with `bashTool: false`, `editTools: false`, `planTool: false` and no skills — `demo-llmman/agent.lucy-reviewer.yaml` is one. A small model with every tool tends to wander (read files, load skills) when it is only asked for an opinion.
55+- **Capture the answer in a variable.** `answer=$("$MM_BIN" --ask jan -p "…")` — stdout holds the answer alone.
56+
57+## See also
58+
59+- Every flag, stream and exit code: [command-line reference](../reference/cli.md#ask-mode-specifics)
60+- `bashTimeout` and `mmConfigs`: [configuration reference](../reference/configuration.md)
61+- Why the agent is a child process, and what `--ask` adds for scripts: [asking another agent — explanation](../explanation/asking-another-agent.md)
new file mode 100644
@@ -0,0 +1,61 @@
1+# How to ask another agent from a skill or a script
2+
3+This guide shows how to make a skill — or any shell script — put a question to another mm agent and use its answer. It assumes the agents are already declared under `mmConfigs` (see [how to ask another agent](ask-another-agent.md)).
4+
5+## Steps
6+
7+1. In the skill, call the same mm back through the variables mm exports to the commands its `bash` tool runs. `MM_BIN` is the running `mm`, and `MM_CONFIG` its configuration, so `--ask` finds the agent names without a path:
8+
9+ ```bash
10+ "$MM_BIN" --ask jan -p "What is the capital of Peru?"
11+ ```
12+
13+ stdout carries the answer and nothing else; the label and the agent's tool calls go to stderr.
14+
15+2. To send the conversation along, write it on stdin, then a `---` line, then the question:
16+
17+ ```bash
18+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
19+ User: I think Go's goroutines are OS threads.
20+ Assistant: They are not: the Go runtime schedules them onto a few OS threads.
21+ ---
22+ Is the assistant right?
23+ CONTEXT
24+ ```
25+
26+ The model writes this summary itself: a command cannot read the conversation.
27+
28+3. Tell the model, in the skill, to run the command once and to report the answer as the other agent's. `demo-llmman/skills/` has three complete examples:
29+
30+ - `delegate-to-jan` — a question delegated to Jan;
31+ - `second-opinion` — the conversation sent to a reviewer;
32+ - `compare-agents` — the same question to Jan and Lucy, in one command.
33+
34+4. Raise the time limits in the asking agent's configuration. A local model can take a minute to answer, and both limits default to well under that:
35+
36+ ```yaml
37+ bashTimeout: 3m # one bash command (default 30s)
38+ watchdogTimeout: 3m # keeps counting while a command runs (ticket 57)
39+ ```
40+
41+5. Run the asking agent from the directory the skills expect and ask in plain words:
42+
43+ ```text
44+ > I think Go's goroutines are OS threads. Get a second opinion from Lucy on that claim.
45+ 📖 read_skill: second-opinion
46+ 🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT' …
47+ │ 🗣 asking reviewer (llmman/hf.co/Menlo/Lucy-gguf:Q4_K_M), with the conversation…
48+ │ The assistant's answer is correct. Go goroutines are not OS threads; …
49+ ```
50+
51+## Variants
52+
53+- **From a terminal or a script, outside mm.** Name the configuration: `mm -config demo-llmman/agent.llmman.yaml --ask lucy -p "…"`, or put the question on stdin. Check the exit code: 0 answered, 1 failed (the reason is on stderr), 130 interrupted.
54+- **An agent that should only answer.** Give it a configuration with `bashTool: false`, `editTools: false`, `planTool: false` and no skills — `demo-llmman/agent.lucy-reviewer.yaml` is one. A small model with every tool tends to wander (read files, load skills) when it is only asked for an opinion.
55+- **Capture the answer in a variable.** `answer=$("$MM_BIN" --ask jan -p "…")` — stdout holds the answer alone.
56+
57+## See also
58+
59+- Every flag, stream and exit code: [command-line reference](../reference/cli.md#ask-mode-specifics)
60+- `bashTimeout` and `mmConfigs`: [configuration reference](../reference/configuration.md)
61+- Why the agent is a child process, and what `--ask` adds for scripts: [asking another agent — explanation](../explanation/asking-another-agent.md)
modified docs/en/reference/cli.md +22 -2
@@ -6,6 +6,8 @@
66
77 ```
88 mm [-config <path>] [-acp] [-tui] [-prompt <text>|-p <text>] [--version] [--about] [<path>]
9+mm [-config <path>] --ask <agent> [-p <question>] (question on stdin when -p is absent)
10+mm [-config <path>] --ask-with-context <agent> [-p <question>] (context on stdin)
911 ```
1012
1113 | Element | Type | Default | Description |
@@ -17,6 +19,8 @@ mm [-config <path>] [-acp] [-tui] [-prompt <text>|-p <text>] [--version] [--abou
1719 | `-prompt <text>` / `-p <text>` | string | `""` | Run a single prompt and exit. The prompt text is required. Works with the config file before or after: `mm -p "hello" config.yaml` and `mm config.yaml -p "hello"` are both valid. No banner, no session persistence, no TUI/ACP. |
1820 | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. |
1921 | `--about` | bool | `false` | Print what `mm` is — one screen: version, the three front ends, the tools, the model servers, where the project and the docs live — and exit, before the configuration is read. |
22+| `--ask <agent>` | string | `""` | Ask the agent `<agent>` (a key of `mmConfigs`) one question and exit; see [Ask mode specifics](#ask-mode-specifics). Like `-p`, works with the config file before or after: `mm agent.yaml --ask lucy -p "…"` and `mm --ask lucy -p "…" agent.yaml` are both valid. |
23+| `--ask-with-context <agent>` | string | `""` | Same, sending a conversation read from stdin before the question. |
2024 | `-h` | — | — | Print the flag usage and exit. |
2125
2226 ## Version
@@ -56,6 +60,8 @@ The first existing source wins.
5660 | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. |
5761 | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. |
5862 | `AGENT_SESSIONS` | `off` turns session persistence off for this run, whatever `sessions.enabled` says. Set by `/ask` on the agents it starts. |
63+| `MM_BIN` | Set by `mm` for the commands its `bash` tool runs: the path of the running `mm` binary. |
64+| `MM_CONFIG` | Set by `mm` for the commands its `bash` tool runs: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. |
5965 | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. |
6066 | Variable named by `apiKeyEnv` | Holds the API key. Default name: none for `dmr`, `LLAMA_API_KEY` for `llamacpp`, `LLMMAN_API_KEY` for `llmman`, `OLLAMA_API_KEY` for `ollama`. |
6167
@@ -117,12 +123,26 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
117123 | Config file | Can appear before or after the `-prompt`/`-p` flag: `mm -p "prompt" config.yaml` and `mm config.yaml -p "prompt"` both work. |
118124 | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. |
119125
126+## Ask mode specifics
127+
128+| Aspect | Behaviour |
129+|--------|-----------|
130+| Configuration | `-config`, the positional path, then `MM_CONFIG`, then the usual resolution. The agent name is looked up in that file's `mmConfigs`. No engine is started for this configuration's own model. |
131+| Question | `-p <question>`; without `-p`, the whole of stdin. |
132+| Context (`--ask-with-context`) | stdin. Without `-p`, stdin is `<context>`, a line `---`, then the question. |
133+| stdout | The agent's answer, and nothing else, followed by a newline. |
134+| stderr | `🗣 asking <agent> (<provider>/<model>)…`, one `🛠️ <agent> · <tool>: <args>` line per tool call, `[ask: …]` on error. |
135+| The agent's tool calls | Allowed, as in the line mode. |
136+| Parent process | Watched every 500 ms: when it is gone, the ask is cancelled and the agent stopped. |
137+| Session | None: neither this process nor the agent writes session files. |
138+
120139 ## Exit codes
121140
122141 | Code | Cause |
123142 |------|-------|
124-| 0 | `/quit`, `Ctrl+C` at the prompt, end of stdin, or the ACP client closed the connection. |
125-| 1 | More than one positional argument; configuration load error; engine initialisation error (unknown provider, required key not set). |
143+| 0 | `/quit`, `Ctrl+C` at the prompt, end of stdin, or the ACP client closed the connection. `--ask`: the agent answered. |
144+| 1 | More than one positional argument; configuration load error; engine initialisation error (unknown provider, required key not set). `--ask`: no question, no context, unknown agent, the agent failed or gave no answer. |
145+| 130 | `--ask`: interrupted (`Ctrl+C`, or the parent process is gone). |
126146
127147 ## Build and install
128148
@@ -6,6 +6,8 @@
6 6
7 ```7 ```
8 mm [-config <path>] [-acp] [-tui] [-prompt <text>|-p <text>] [--version] [--about] [<path>]8 mm [-config <path>] [-acp] [-tui] [-prompt <text>|-p <text>] [--version] [--about] [<path>]
9+mm [-config <path>] --ask <agent> [-p <question>] (question on stdin when -p is absent)
10+mm [-config <path>] --ask-with-context <agent> [-p <question>] (context on stdin)
9 ```11 ```
10 12
11 | Element | Type | Default | Description |13 | Element | Type | Default | Description |
@@ -17,6 +19,8 @@ mm [-config <path>] [-acp] [-tui] [-prompt <text>|-p <text>] [--version] [--abou
17 | `-prompt <text>` / `-p <text>` | string | `""` | Run a single prompt and exit. The prompt text is required. Works with the config file before or after: `mm -p "hello" config.yaml` and `mm config.yaml -p "hello"` are both valid. No banner, no session persistence, no TUI/ACP. |19 | `-prompt <text>` / `-p <text>` | string | `""` | Run a single prompt and exit. The prompt text is required. Works with the config file before or after: `mm -p "hello" config.yaml` and `mm config.yaml -p "hello"` are both valid. No banner, no session persistence, no TUI/ACP. |
18 | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. |20 | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. |
19 | `--about` | bool | `false` | Print what `mm` is — one screen: version, the three front ends, the tools, the model servers, where the project and the docs live — and exit, before the configuration is read. |21 | `--about` | bool | `false` | Print what `mm` is — one screen: version, the three front ends, the tools, the model servers, where the project and the docs live — and exit, before the configuration is read. |
22+| `--ask <agent>` | string | `""` | Ask the agent `<agent>` (a key of `mmConfigs`) one question and exit; see [Ask mode specifics](#ask-mode-specifics). Like `-p`, works with the config file before or after: `mm agent.yaml --ask lucy -p "…"` and `mm --ask lucy -p "…" agent.yaml` are both valid. |
23+| `--ask-with-context <agent>` | string | `""` | Same, sending a conversation read from stdin before the question. |
20 | `-h` | — | — | Print the flag usage and exit. |24 | `-h` | — | — | Print the flag usage and exit. |
21 25
22 ## Version26 ## Version
@@ -56,6 +60,8 @@ The first existing source wins.
56 | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. |60 | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. |
57 | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. |61 | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. |
58 | `AGENT_SESSIONS` | `off` turns session persistence off for this run, whatever `sessions.enabled` says. Set by `/ask` on the agents it starts. |62 | `AGENT_SESSIONS` | `off` turns session persistence off for this run, whatever `sessions.enabled` says. Set by `/ask` on the agents it starts. |
63+| `MM_BIN` | Set by `mm` for the commands its `bash` tool runs: the path of the running `mm` binary. |
64+| `MM_CONFIG` | Set by `mm` for the commands its `bash` tool runs: the absolute path of its configuration file. Read by `--ask` / `--ask-with-context` when no `-config` and no positional path are given. |
59 | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. |65 | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. |
60 | Variable named by `apiKeyEnv` | Holds the API key. Default name: none for `dmr`, `LLAMA_API_KEY` for `llamacpp`, `LLMMAN_API_KEY` for `llmman`, `OLLAMA_API_KEY` for `ollama`. |66 | Variable named by `apiKeyEnv` | Holds the API key. Default name: none for `dmr`, `LLAMA_API_KEY` for `llamacpp`, `LLMMAN_API_KEY` for `llmman`, `OLLAMA_API_KEY` for `ollama`. |
61 67
@@ -117,12 +123,26 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
117 | Config file | Can appear before or after the `-prompt`/`-p` flag: `mm -p "prompt" config.yaml` and `mm config.yaml -p "prompt"` both work. |123 | Config file | Can appear before or after the `-prompt`/`-p` flag: `mm -p "prompt" config.yaml` and `mm config.yaml -p "prompt"` both work. |
118 | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. |124 | Compatibility | Incompatible with `-acp` and `-tui`; prompt mode takes precedence and runs regardless. |
119 125
126+## Ask mode specifics
127+
128+| Aspect | Behaviour |
129+|--------|-----------|
130+| Configuration | `-config`, the positional path, then `MM_CONFIG`, then the usual resolution. The agent name is looked up in that file's `mmConfigs`. No engine is started for this configuration's own model. |
131+| Question | `-p <question>`; without `-p`, the whole of stdin. |
132+| Context (`--ask-with-context`) | stdin. Without `-p`, stdin is `<context>`, a line `---`, then the question. |
133+| stdout | The agent's answer, and nothing else, followed by a newline. |
134+| stderr | `🗣 asking <agent> (<provider>/<model>)…`, one `🛠️ <agent> · <tool>: <args>` line per tool call, `[ask: …]` on error. |
135+| The agent's tool calls | Allowed, as in the line mode. |
136+| Parent process | Watched every 500 ms: when it is gone, the ask is cancelled and the agent stopped. |
137+| Session | None: neither this process nor the agent writes session files. |
138+
120 ## Exit codes139 ## Exit codes
121 140
122 | Code | Cause |141 | Code | Cause |
123 |------|-------|142 |------|-------|
124-| 0 | `/quit`, `Ctrl+C` at the prompt, end of stdin, or the ACP client closed the connection. |143+| 0 | `/quit`, `Ctrl+C` at the prompt, end of stdin, or the ACP client closed the connection. `--ask`: the agent answered. |
125-| 1 | More than one positional argument; configuration load error; engine initialisation error (unknown provider, required key not set). |144+| 1 | More than one positional argument; configuration load error; engine initialisation error (unknown provider, required key not set). `--ask`: no question, no context, unknown agent, the agent failed or gave no answer. |
145+| 130 | `--ask`: interrupted (`Ctrl+C`, or the parent process is gone). |
126 146
127 ## Build and install147 ## Build and install
128 148
modified docs/en/reference/configuration.md +2 -0
@@ -25,6 +25,7 @@
2525 | `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). |
2626 | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. |
2727 | `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama sends no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). |
28+| `bashTimeout` | duration | `30s` | How long one `bash` command may run before it is killed; the tool then answers `[error: <bashTimeout> timeout exceeded]`. Must be > 0. The watchdog keeps counting while a command runs (ticket 57): a command longer than `watchdogTimeout` ends the turn, so raise both together. |
2829 | `context` | map | see below | Automatic history compression. |
2930 | `sessions` | map | see below | Persistent, per-project sessions. |
3031 | `acp` | map | see below | How `mm -acp` introduces itself to the editor. |
@@ -118,6 +119,7 @@ Reported as `[config error: <path>: <message>]`, exit code 1.
118119 | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | an extension without its dot |
119120 | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | two servers on one extension |
120121 | `permissions must be "auto" or "ask"` | any other value |
122+| `bashTimeout must be > 0` | zero or negative |
121123 | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` |
122124 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |
123125 | `mmConfigs.<name>: the path must not be empty` | `<name>: ""` |
@@ -25,6 +25,7 @@
25 | `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). |25 | `sampling` | map | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Generation settings, OpenAI API key names, passed to the server. The shipped `agent.yaml` adds `parallel_tool_calls: false`. `max_tokens` also bounds one tool call: a `write_file` of a long document (or a thinking model's reasoning plus the call) can hit it, and the call is then cut off. mm hands it back to the model twice, asking for smaller pieces, then stops with `the model hit max_tokens (N) while writing a tool call`; raise the value if that happens. With streaming, mm also asks the server for token counts (`stream_options.include_usage`). |
26 | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. |26 | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. |
27 | `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama sends no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). |27 | `firstTokenTimeout` | duration | `3m` | Silence tolerated before a model call's first token, while the server reads the prompt (every round trip of a turn has one). Never shorter than `watchdogTimeout`; must be >= 0. With a llama.cpp server (`dmr`, `llamacpp`, `llmman` — whose local GGUF models run in llama-server) mm asks for `return_progress`: each progress report restarts this clock, and the spinner shows `Reading the prompt 12.0k/25.3k tokens`. Ollama sends no progress: this one wait covers the whole prompt (and the model's loading). On a timeout the watchdog says `no first token from the server after …` (before the first token) or `connection hang detected after …` (after). |
28+| `bashTimeout` | duration | `30s` | How long one `bash` command may run before it is killed; the tool then answers `[error: <bashTimeout> timeout exceeded]`. Must be > 0. The watchdog keeps counting while a command runs (ticket 57): a command longer than `watchdogTimeout` ends the turn, so raise both together. |
28 | `context` | map | see below | Automatic history compression. |29 | `context` | map | see below | Automatic history compression. |
29 | `sessions` | map | see below | Persistent, per-project sessions. |30 | `sessions` | map | see below | Persistent, per-project sessions. |
30 | `acp` | map | see below | How `mm -acp` introduces itself to the editor. |31 | `acp` | map | see below | How `mm -acp` introduces itself to the editor. |
@@ -118,6 +119,7 @@ Reported as `[config error: <path>: <message>]`, exit code 1.
118 | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | an extension without its dot |119 | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | an extension without its dot |
119 | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | two servers on one extension |120 | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | two servers on one extension |
120 | `permissions must be "auto" or "ask"` | any other value |121 | `permissions must be "auto" or "ask"` | any other value |
122+| `bashTimeout must be > 0` | zero or negative |
121 | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` |123 | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` |
122 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |124 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |
123 | `mmConfigs.<name>: the path must not be empty` | `<name>: ""` |125 | `mmConfigs.<name>: the path must not be empty` | `<name>: ""` |
modified docs/fr/README.md +1 -0
@@ -14,6 +14,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https:
1414 - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demo-llmman/`
1515 - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demo-ollama/`
1616 - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>`
17+- [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, les skills de démo
1718 - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md)
1819 - [Faire tourner mini-me dans un Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — le template et les deux kits sous `docker-sbx/`, mode fenêtre et éditeur
1920 - [Faire tourner mini-me avec les kits sandbox v3](how-to/run-with-sandbox-kits-v3.md) — les deux mêmes kits pour la spécification de kit v3, sous `docker-sbx-v3/`
@@ -14,6 +14,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https:
14 - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demo-llmman/`14 - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demo-llmman/`
15 - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demo-ollama/`15 - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demo-ollama/`
16 - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>`16 - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>`
17+- [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, les skills de démo
17 - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md)18 - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md)
18 - [Faire tourner mini-me dans un Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — le template et les deux kits sous `docker-sbx/`, mode fenêtre et éditeur19 - [Faire tourner mini-me dans un Docker Sandbox](how-to/run-in-a-docker-sandbox.md) — le template et les deux kits sous `docker-sbx/`, mode fenêtre et éditeur
19 - [Faire tourner mini-me avec les kits sandbox v3](how-to/run-with-sandbox-kits-v3.md) — les deux mêmes kits pour la spécification de kit v3, sous `docker-sbx-v3/`20 - [Faire tourner mini-me avec les kits sandbox v3](how-to/run-with-sandbox-kits-v3.md) — les deux mêmes kits pour la spécification de kit v3, sous `docker-sbx-v3/`
modified docs/fr/explanation/asking-another-agent.md +4 -0
@@ -22,6 +22,10 @@
2222
2323 **Les chaînes s'arrêtent.** `/ask` est une commande tapée par une personne ; aucun modèle ne peut l'appeler, donc aucun agent ne boucle tout seul. Une personne pourrait quand même taper `/ask` dans un agent lui-même interrogé, alors chaque enfant connaît sa profondeur (`MM_ASK_DEPTH`) : votre agent peut interroger bob, bob peut interroger riker, riker ne peut interroger personne.
2424
25+**La même mécanique répond aux scripts.** `mm --ask <agent>` et `mm --ask-with-context <agent>` sont les commandes slash de ce qui ne sait que lancer une commande — une skill, un script. Ils démarrent le même enfant en ACP, avec la même protection de l'environnement et la même limite de profondeur, mais la réponse va sur stdout et tout le reste sur stderr, pour que l'appelant récupère la réponse seule. mm exporte `MM_BIN` et `MM_CONFIG` vers les commandes de son outil `bash`, donc une skill appelle « ce mm, avec cette configuration » sans connaître aucun des deux chemins. Deux choses diffèrent des commandes slash, parce que l'appelant est une commande et non une personne. Les appels d'outils de l'agent sont autorisés, comme en mode ligne : la personne a déjà validé la commande `mm --ask` elle-même. Et `mm --ask` surveille son processus parent : l'outil `bash` tue `bash` quand son budget est écoulé, pas les enfants de bash, et une demande orpheline laissait son agent tourner — et écrire des fichiers — alors que plus personne n'attendait.
26+
27+**Le temps est le point faible.** Un modèle local peut mettre une minute à répondre, et une commande `bash` disposait de 30 secondes ; `bashTimeout` règle désormais ce budget. Le watchdog de l'agent qui pose la question continue aussi de compter pendant qu'une commande tourne (ticket 57), il faut donc le relever aussi tant que ce ticket n'est pas corrigé.
28+
2529 ## Alternatives écartées
2630
2731 - **`mm -p "<prompt>"` en sous-processus.** Un processus par question et rien à garder en vie — mais sa sortie est un écran de terminal (réponse, raisonnement, échos d'outils, couleurs, récapitulatif des commandes) qu'il faudrait analyser ; il ne demande jamais de permission, donc bob lancerait des commandes sans contrôle ; on ne peut l'annuler qu'en le tuant.
@@ -22,6 +22,10 @@
22 22
23 **Les chaînes s'arrêtent.** `/ask` est une commande tapée par une personne ; aucun modèle ne peut l'appeler, donc aucun agent ne boucle tout seul. Une personne pourrait quand même taper `/ask` dans un agent lui-même interrogé, alors chaque enfant connaît sa profondeur (`MM_ASK_DEPTH`) : votre agent peut interroger bob, bob peut interroger riker, riker ne peut interroger personne.23 **Les chaînes s'arrêtent.** `/ask` est une commande tapée par une personne ; aucun modèle ne peut l'appeler, donc aucun agent ne boucle tout seul. Une personne pourrait quand même taper `/ask` dans un agent lui-même interrogé, alors chaque enfant connaît sa profondeur (`MM_ASK_DEPTH`) : votre agent peut interroger bob, bob peut interroger riker, riker ne peut interroger personne.
24 24
25+**La même mécanique répond aux scripts.** `mm --ask <agent>` et `mm --ask-with-context <agent>` sont les commandes slash de ce qui ne sait que lancer une commande — une skill, un script. Ils démarrent le même enfant en ACP, avec la même protection de l'environnement et la même limite de profondeur, mais la réponse va sur stdout et tout le reste sur stderr, pour que l'appelant récupère la réponse seule. mm exporte `MM_BIN` et `MM_CONFIG` vers les commandes de son outil `bash`, donc une skill appelle « ce mm, avec cette configuration » sans connaître aucun des deux chemins. Deux choses diffèrent des commandes slash, parce que l'appelant est une commande et non une personne. Les appels d'outils de l'agent sont autorisés, comme en mode ligne : la personne a déjà validé la commande `mm --ask` elle-même. Et `mm --ask` surveille son processus parent : l'outil `bash` tue `bash` quand son budget est écoulé, pas les enfants de bash, et une demande orpheline laissait son agent tourner — et écrire des fichiers — alors que plus personne n'attendait.
26+
27+**Le temps est le point faible.** Un modèle local peut mettre une minute à répondre, et une commande `bash` disposait de 30 secondes ; `bashTimeout` règle désormais ce budget. Le watchdog de l'agent qui pose la question continue aussi de compter pendant qu'une commande tourne (ticket 57), il faut donc le relever aussi tant que ce ticket n'est pas corrigé.
28+
25 ## Alternatives écartées29 ## Alternatives écartées
26 30
27 - **`mm -p "<prompt>"` en sous-processus.** Un processus par question et rien à garder en vie — mais sa sortie est un écran de terminal (réponse, raisonnement, échos d'outils, couleurs, récapitulatif des commandes) qu'il faudrait analyser ; il ne demande jamais de permission, donc bob lancerait des commandes sans contrôle ; on ne peut l'annuler qu'en le tuant.31 - **`mm -p "<prompt>"` en sous-processus.** Un processus par question et rien à garder en vie — mais sa sortie est un écran de terminal (réponse, raisonnement, échos d'outils, couleurs, récapitulatif des commandes) qu'il faudrait analyser ; il ne demande jamais de permission, donc bob lancerait des commandes sans contrôle ; on ne peut l'annuler qu'en le tuant.
added docs/fr/how-to/ask-another-agent-from-a-skill.md +61 -0
new file mode 100644
@@ -0,0 +1,61 @@
1+# Comment interroger un autre agent depuis une skill ou un script
2+
3+Ce guide montre comment faire poser une question à un autre agent mm par une skill — ou par n'importe quel script shell — et se servir de sa réponse. Il suppose que les agents sont déjà déclarés sous `mmConfigs` (voir [comment interroger un autre agent](ask-another-agent.md)).
4+
5+## Étapes
6+
7+1. Dans la skill, rappelez le même mm via les variables que mm exporte vers les commandes de son outil `bash`. `MM_BIN` est le `mm` en cours, et `MM_CONFIG` sa configuration : `--ask` trouve donc les noms des agents sans chemin :
8+
9+ ```bash
10+ "$MM_BIN" --ask jan -p "What is the capital of Peru?"
11+ ```
12+
13+ stdout porte la réponse et rien d'autre ; l'en-tête et les appels d'outils de l'agent vont sur stderr.
14+
15+2. Pour envoyer la conversation, écrivez-la sur stdin, puis une ligne `---`, puis la question :
16+
17+ ```bash
18+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
19+ User: I think Go's goroutines are OS threads.
20+ Assistant: They are not: the Go runtime schedules them onto a few OS threads.
21+ ---
22+ Is the assistant right?
23+ CONTEXT
24+ ```
25+
26+ C'est le modèle qui écrit ce résumé lui-même : une commande ne peut pas lire la conversation.
27+
28+3. Dites au modèle, dans la skill, de lancer la commande une seule fois et de présenter la réponse comme celle de l'autre agent. `demo-llmman/skills/` en contient trois exemples complets :
29+
30+ - `delegate-to-jan` — une question déléguée à Jan ;
31+ - `second-opinion` — la conversation envoyée à un relecteur ;
32+ - `compare-agents` — la même question à Jan et à Lucy, en une seule commande.
33+
34+4. Relevez les délais dans la configuration de l'agent qui pose la question. Un modèle local peut mettre une minute à répondre, et les deux délais par défaut sont bien en dessous :
35+
36+ ```yaml
37+ bashTimeout: 3m # une commande bash (30s par défaut)
38+ watchdogTimeout: 3m # continue de compter pendant qu'une commande tourne (ticket 57)
39+ ```
40+
41+5. Lancez l'agent depuis le dossier qu'attendent les skills et demandez en langage courant :
42+
43+ ```text
44+ > I think Go's goroutines are OS threads. Get a second opinion from Lucy on that claim.
45+ 📖 read_skill: second-opinion
46+ 🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT' …
47+ │ 🗣 asking reviewer (llmman/hf.co/Menlo/Lucy-gguf:Q4_K_M), with the conversation…
48+ │ The assistant's answer is correct. Go goroutines are not OS threads; …
49+ ```
50+
51+## Variantes
52+
53+- **Depuis un terminal ou un script, hors de mm.** Nommez la configuration : `mm -config demo-llmman/agent.llmman.yaml --ask lucy -p "…"`, ou mettez la question sur stdin. Vérifiez le code de sortie : 0 réponse obtenue, 1 échec (la raison est sur stderr), 130 interrompu.
54+- **Un agent qui doit seulement répondre.** Donnez-lui une configuration avec `bashTool: false`, `editTools: false`, `planTool: false` et sans skills — `demo-llmman/agent.lucy-reviewer.yaml` en est une. Un petit modèle doté de tous ses outils a tendance à s'égarer (lire des fichiers, charger des skills) quand on lui demande seulement son avis.
55+- **Récupérer la réponse dans une variable.** `answer=$("$MM_BIN" --ask jan -p "…")` — stdout ne contient que la réponse.
56+
57+## Voir aussi
58+
59+- Toutes les options, flux et codes de sortie : [référence de la ligne de commande](../reference/cli.md#spécificités-du-mode-ask)
60+- `bashTimeout` et `mmConfigs` : [référence de configuration](../reference/configuration.md)
61+- Pourquoi l'agent est un processus enfant, et ce que `--ask` ajoute pour les scripts : [interroger un autre agent — explication](../explanation/asking-another-agent.md)
new file mode 100644
@@ -0,0 +1,61 @@
1+# Comment interroger un autre agent depuis une skill ou un script
2+
3+Ce guide montre comment faire poser une question à un autre agent mm par une skill — ou par n'importe quel script shell — et se servir de sa réponse. Il suppose que les agents sont déjà déclarés sous `mmConfigs` (voir [comment interroger un autre agent](ask-another-agent.md)).
4+
5+## Étapes
6+
7+1. Dans la skill, rappelez le même mm via les variables que mm exporte vers les commandes de son outil `bash`. `MM_BIN` est le `mm` en cours, et `MM_CONFIG` sa configuration : `--ask` trouve donc les noms des agents sans chemin :
8+
9+ ```bash
10+ "$MM_BIN" --ask jan -p "What is the capital of Peru?"
11+ ```
12+
13+ stdout porte la réponse et rien d'autre ; l'en-tête et les appels d'outils de l'agent vont sur stderr.
14+
15+2. Pour envoyer la conversation, écrivez-la sur stdin, puis une ligne `---`, puis la question :
16+
17+ ```bash
18+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
19+ User: I think Go's goroutines are OS threads.
20+ Assistant: They are not: the Go runtime schedules them onto a few OS threads.
21+ ---
22+ Is the assistant right?
23+ CONTEXT
24+ ```
25+
26+ C'est le modèle qui écrit ce résumé lui-même : une commande ne peut pas lire la conversation.
27+
28+3. Dites au modèle, dans la skill, de lancer la commande une seule fois et de présenter la réponse comme celle de l'autre agent. `demo-llmman/skills/` en contient trois exemples complets :
29+
30+ - `delegate-to-jan` — une question déléguée à Jan ;
31+ - `second-opinion` — la conversation envoyée à un relecteur ;
32+ - `compare-agents` — la même question à Jan et à Lucy, en une seule commande.
33+
34+4. Relevez les délais dans la configuration de l'agent qui pose la question. Un modèle local peut mettre une minute à répondre, et les deux délais par défaut sont bien en dessous :
35+
36+ ```yaml
37+ bashTimeout: 3m # une commande bash (30s par défaut)
38+ watchdogTimeout: 3m # continue de compter pendant qu'une commande tourne (ticket 57)
39+ ```
40+
41+5. Lancez l'agent depuis le dossier qu'attendent les skills et demandez en langage courant :
42+
43+ ```text
44+ > I think Go's goroutines are OS threads. Get a second opinion from Lucy on that claim.
45+ 📖 read_skill: second-opinion
46+ 🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT' …
47+ │ 🗣 asking reviewer (llmman/hf.co/Menlo/Lucy-gguf:Q4_K_M), with the conversation…
48+ │ The assistant's answer is correct. Go goroutines are not OS threads; …
49+ ```
50+
51+## Variantes
52+
53+- **Depuis un terminal ou un script, hors de mm.** Nommez la configuration : `mm -config demo-llmman/agent.llmman.yaml --ask lucy -p "…"`, ou mettez la question sur stdin. Vérifiez le code de sortie : 0 réponse obtenue, 1 échec (la raison est sur stderr), 130 interrompu.
54+- **Un agent qui doit seulement répondre.** Donnez-lui une configuration avec `bashTool: false`, `editTools: false`, `planTool: false` et sans skills — `demo-llmman/agent.lucy-reviewer.yaml` en est une. Un petit modèle doté de tous ses outils a tendance à s'égarer (lire des fichiers, charger des skills) quand on lui demande seulement son avis.
55+- **Récupérer la réponse dans une variable.** `answer=$("$MM_BIN" --ask jan -p "…")` — stdout ne contient que la réponse.
56+
57+## Voir aussi
58+
59+- Toutes les options, flux et codes de sortie : [référence de la ligne de commande](../reference/cli.md#spécificités-du-mode-ask)
60+- `bashTimeout` et `mmConfigs` : [référence de configuration](../reference/configuration.md)
61+- Pourquoi l'agent est un processus enfant, et ce que `--ask` ajoute pour les scripts : [interroger un autre agent — explication](../explanation/asking-another-agent.md)
modified docs/fr/reference/cli.md +22 -2
@@ -6,6 +6,8 @@
66
77 ```
88 mm [-config <chemin>] [-acp] [-tui] [-prompt <texte>|-p <texte>] [--version] [--about] [<chemin>]
9+mm [-config <chemin>] --ask <agent> [-p <question>] (question sur stdin sans -p)
10+mm [-config <chemin>] --ask-with-context <agent> [-p <question>] (contexte sur stdin)
911 ```
1012
1113 | Élément | Type | Défaut | Description |
@@ -17,6 +19,8 @@ mm [-config <chemin>] [-acp] [-tui] [-prompt <texte>|-p <texte>] [--version] [--
1719 | `-prompt <texte>` / `-p <texte>` | chaîne | `""` | Exécute une seule invite et quitte. Le texte de l'invite est obligatoire. Fonctionne avec le fichier de config avant ou après : `mm -p "bonjour" config.yaml` et `mm config.yaml -p "bonjour"` sont tous deux valides. Pas de bannière, pas de persistance de session, pas de TUI/ACP. |
1820 | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. |
1921 | `--about` | bool | `false` | Affiche ce qu'est `mm` — un écran : version, les trois façades, les outils, les serveurs de modèles, où vivent le projet et la documentation — puis quitte, avant la lecture de la configuration. |
22+| `--ask <agent>` | chaîne | `""` | Pose une question à l'agent `<agent>` (une clé de `mmConfigs`) et quitte ; voir [Spécificités du mode ask](#spécificités-du-mode-ask). Comme `-p`, fonctionne avec le fichier de configuration avant ou après : `mm agent.yaml --ask lucy -p "…"` et `mm --ask lucy -p "…" agent.yaml` sont tous deux valides. |
23+| `--ask-with-context <agent>` | chaîne | `""` | Pareil, en envoyant avant la question une conversation lue sur stdin. |
2024 | `-h` | — | — | Affiche l'usage des options et quitte. |
2125
2226 ## Version
@@ -56,6 +60,8 @@ La première source existante gagne.
5660 | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. |
5761 | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. |
5862 | `AGENT_SESSIONS` | `off` désactive l'enregistrement des sessions pour cette exécution, quoi que dise `sessions.enabled`. Posée par `/ask` sur les agents qu'il lance. |
63+| `MM_BIN` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin du binaire `mm` en cours. |
64+| `MM_CONFIG` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. |
5965 | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. |
6066 | Variable nommée par `apiKeyEnv` | Contient la clé d'API. Nom par défaut : aucun pour `dmr`, `LLAMA_API_KEY` pour `llamacpp`, `LLMMAN_API_KEY` pour `llmman`, `OLLAMA_API_KEY` pour `ollama`. |
6167
@@ -117,12 +123,26 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
117123 | Fichier de config | Peut apparaître avant ou après le flag `-prompt`/`-p` : `mm -p "prompt" config.yaml` et `mm config.yaml -p "prompt"` fonctionnent tous deux. |
118124 | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. |
119125
126+## Spécificités du mode ask
127+
128+| Aspect | Comportement |
129+|--------|--------------|
130+| Configuration | `-config`, le chemin positionnel, puis `MM_CONFIG`, puis la résolution habituelle. Le nom de l'agent est cherché dans les `mmConfigs` de ce fichier. Aucun moteur n'est démarré pour le modèle propre à cette configuration. |
131+| Question | `-p <question>` ; sans `-p`, tout stdin. |
132+| Contexte (`--ask-with-context`) | stdin. Sans `-p`, stdin contient `<contexte>`, une ligne `---`, puis la question. |
133+| stdout | La réponse de l'agent, et rien d'autre, suivie d'un saut de ligne. |
134+| stderr | `🗣 asking <agent> (<fournisseur>/<modèle>)…`, une ligne `🛠️ <agent> · <outil>: <arguments>` par appel d'outil, `[ask: …]` en cas d'erreur. |
135+| Appels d'outils de l'agent | Autorisés, comme en mode ligne. |
136+| Processus parent | Surveillé toutes les 500 ms : s'il disparaît, la demande est annulée et l'agent arrêté. |
137+| Session | Aucune : ni ce processus ni l'agent n'écrivent de fichier de session. |
138+
120139 ## Codes de sortie
121140
122141 | Code | Cause |
123142 |------|-------|
124-| 0 | `/quit`, `Ctrl+C` à l'invite, fin de stdin, ou le client ACP a fermé la connexion. |
125-| 1 | Plus d'un argument positionnel ; erreur de chargement de la configuration ; erreur d'initialisation du moteur (fournisseur inconnu, clé requise absente). |
143+| 0 | `/quit`, `Ctrl+C` à l'invite, fin de stdin, ou le client ACP a fermé la connexion. `--ask` : l'agent a répondu. |
144+| 1 | Plus d'un argument positionnel ; erreur de chargement de la configuration ; erreur d'initialisation du moteur (fournisseur inconnu, clé requise absente). `--ask` : pas de question, pas de contexte, agent inconnu, l'agent a échoué ou n'a rien répondu. |
145+| 130 | `--ask` : interrompu (`Ctrl+C`, ou le processus parent a disparu). |
126146
127147 ## Compilation et installation
128148
@@ -6,6 +6,8 @@
6 6
7 ```7 ```
8 mm [-config <chemin>] [-acp] [-tui] [-prompt <texte>|-p <texte>] [--version] [--about] [<chemin>]8 mm [-config <chemin>] [-acp] [-tui] [-prompt <texte>|-p <texte>] [--version] [--about] [<chemin>]
9+mm [-config <chemin>] --ask <agent> [-p <question>] (question sur stdin sans -p)
10+mm [-config <chemin>] --ask-with-context <agent> [-p <question>] (contexte sur stdin)
9 ```11 ```
10 12
11 | Élément | Type | Défaut | Description |13 | Élément | Type | Défaut | Description |
@@ -17,6 +19,8 @@ mm [-config <chemin>] [-acp] [-tui] [-prompt <texte>|-p <texte>] [--version] [--
17 | `-prompt <texte>` / `-p <texte>` | chaîne | `""` | Exécute une seule invite et quitte. Le texte de l'invite est obligatoire. Fonctionne avec le fichier de config avant ou après : `mm -p "bonjour" config.yaml` et `mm config.yaml -p "bonjour"` sont tous deux valides. Pas de bannière, pas de persistance de session, pas de TUI/ACP. |19 | `-prompt <texte>` / `-p <texte>` | chaîne | `""` | Exécute une seule invite et quitte. Le texte de l'invite est obligatoire. Fonctionne avec le fichier de config avant ou après : `mm -p "bonjour" config.yaml` et `mm config.yaml -p "bonjour"` sont tous deux valides. Pas de bannière, pas de persistance de session, pas de TUI/ACP. |
18 | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. |20 | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. |
19 | `--about` | bool | `false` | Affiche ce qu'est `mm` — un écran : version, les trois façades, les outils, les serveurs de modèles, où vivent le projet et la documentation — puis quitte, avant la lecture de la configuration. |21 | `--about` | bool | `false` | Affiche ce qu'est `mm` — un écran : version, les trois façades, les outils, les serveurs de modèles, où vivent le projet et la documentation — puis quitte, avant la lecture de la configuration. |
22+| `--ask <agent>` | chaîne | `""` | Pose une question à l'agent `<agent>` (une clé de `mmConfigs`) et quitte ; voir [Spécificités du mode ask](#spécificités-du-mode-ask). Comme `-p`, fonctionne avec le fichier de configuration avant ou après : `mm agent.yaml --ask lucy -p "…"` et `mm --ask lucy -p "…" agent.yaml` sont tous deux valides. |
23+| `--ask-with-context <agent>` | chaîne | `""` | Pareil, en envoyant avant la question une conversation lue sur stdin. |
20 | `-h` | — | — | Affiche l'usage des options et quitte. |24 | `-h` | — | — | Affiche l'usage des options et quitte. |
21 25
22 ## Version26 ## Version
@@ -56,6 +60,8 @@ La première source existante gagne.
56 | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. |60 | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. |
57 | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. |61 | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. |
58 | `AGENT_SESSIONS` | `off` désactive l'enregistrement des sessions pour cette exécution, quoi que dise `sessions.enabled`. Posée par `/ask` sur les agents qu'il lance. |62 | `AGENT_SESSIONS` | `off` désactive l'enregistrement des sessions pour cette exécution, quoi que dise `sessions.enabled`. Posée par `/ask` sur les agents qu'il lance. |
63+| `MM_BIN` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin du binaire `mm` en cours. |
64+| `MM_CONFIG` | Posée par `mm` pour les commandes que lance son outil `bash` : le chemin absolu de son fichier de configuration. Lue par `--ask` / `--ask-with-context` quand ni `-config` ni chemin positionnel ne sont donnés. |
59 | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. |65 | `MM_ASK_DEPTH` | Nombre de niveaux de `/ask` au-dessus de cet agent (absente = 0). Posée par `/ask` ; à 2, `/ask` est refusée. |
60 | Variable nommée par `apiKeyEnv` | Contient la clé d'API. Nom par défaut : aucun pour `dmr`, `LLAMA_API_KEY` pour `llamacpp`, `LLMMAN_API_KEY` pour `llmman`, `OLLAMA_API_KEY` pour `ollama`. |66 | Variable nommée par `apiKeyEnv` | Contient la clé d'API. Nom par défaut : aucun pour `dmr`, `LLAMA_API_KEY` pour `llamacpp`, `LLMMAN_API_KEY` pour `llmman`, `OLLAMA_API_KEY` pour `ollama`. |
61 67
@@ -117,12 +123,26 @@ mm v0.1.7 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/
117 | Fichier de config | Peut apparaître avant ou après le flag `-prompt`/`-p` : `mm -p "prompt" config.yaml` et `mm config.yaml -p "prompt"` fonctionnent tous deux. |123 | Fichier de config | Peut apparaître avant ou après le flag `-prompt`/`-p` : `mm -p "prompt" config.yaml` et `mm config.yaml -p "prompt"` fonctionnent tous deux. |
118 | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. |124 | Compatibilité | Incompatible avec `-acp` et `-tui` ; le mode invite prend la priorité et s'exécute dans tous les cas. |
119 125
126+## Spécificités du mode ask
127+
128+| Aspect | Comportement |
129+|--------|--------------|
130+| Configuration | `-config`, le chemin positionnel, puis `MM_CONFIG`, puis la résolution habituelle. Le nom de l'agent est cherché dans les `mmConfigs` de ce fichier. Aucun moteur n'est démarré pour le modèle propre à cette configuration. |
131+| Question | `-p <question>` ; sans `-p`, tout stdin. |
132+| Contexte (`--ask-with-context`) | stdin. Sans `-p`, stdin contient `<contexte>`, une ligne `---`, puis la question. |
133+| stdout | La réponse de l'agent, et rien d'autre, suivie d'un saut de ligne. |
134+| stderr | `🗣 asking <agent> (<fournisseur>/<modèle>)…`, une ligne `🛠️ <agent> · <outil>: <arguments>` par appel d'outil, `[ask: …]` en cas d'erreur. |
135+| Appels d'outils de l'agent | Autorisés, comme en mode ligne. |
136+| Processus parent | Surveillé toutes les 500 ms : s'il disparaît, la demande est annulée et l'agent arrêté. |
137+| Session | Aucune : ni ce processus ni l'agent n'écrivent de fichier de session. |
138+
120 ## Codes de sortie139 ## Codes de sortie
121 140
122 | Code | Cause |141 | Code | Cause |
123 |------|-------|142 |------|-------|
124-| 0 | `/quit`, `Ctrl+C` à l'invite, fin de stdin, ou le client ACP a fermé la connexion. |143+| 0 | `/quit`, `Ctrl+C` à l'invite, fin de stdin, ou le client ACP a fermé la connexion. `--ask` : l'agent a répondu. |
125-| 1 | Plus d'un argument positionnel ; erreur de chargement de la configuration ; erreur d'initialisation du moteur (fournisseur inconnu, clé requise absente). |144+| 1 | Plus d'un argument positionnel ; erreur de chargement de la configuration ; erreur d'initialisation du moteur (fournisseur inconnu, clé requise absente). `--ask` : pas de question, pas de contexte, agent inconnu, l'agent a échoué ou n'a rien répondu. |
145+| 130 | `--ask` : interrompu (`Ctrl+C`, ou le processus parent a disparu). |
126 146
127 ## Compilation et installation147 ## Compilation et installation
128 148
modified docs/fr/reference/configuration.md +2 -0
@@ -25,6 +25,7 @@
2525 | `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). |
2626 | `watchdogTimeout` | durée | `20s` | Silence toléré entre deux tokens diffusés avant de déclarer la connexion bloquée. Doublé pour les requêtes de résumé. |
2727 | `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama n'envoie pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). |
28+| `bashTimeout` | durée | `30s` | Durée maximale d'une commande `bash` avant qu'elle soit tuée ; l'outil répond alors `[error: <bashTimeout> timeout exceeded]`. Doit être > 0. Le watchdog continue de compter pendant qu'une commande tourne (ticket 57) : une commande plus longue que `watchdogTimeout` termine le tour, donc relevez les deux ensemble. |
2829 | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. |
2930 | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. |
3031 | `acp` | dictionnaire | voir ci-dessous | La façon dont `mm -acp` se présente à l'éditeur. |
@@ -118,6 +119,7 @@ Signalées sous la forme `[config error: <chemin>: <message>]`, code de sortie 1
118119 | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | une extension sans son point |
119120 | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | deux serveurs sur une même extension |
120121 | `permissions must be "auto" or "ask"` | toute autre valeur |
122+| `bashTimeout must be > 0` | zéro ou négative |
121123 | `mmConfigs: "Bob" is not a valid name (…)` | un nom avec des majuscules, des espaces ou d'autres caractères, ou commençant par `-` |
122124 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` |
123125 | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""` |
@@ -25,6 +25,7 @@
25 | `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). |25 | `sampling` | dictionnaire | `temperature: 0.0`, `top_p: 0.9`, `max_tokens: 4096` | Paramètres de génération, noms de l'API OpenAI, transmis au serveur. L'`agent.yaml` livré ajoute `parallel_tool_calls: false`. `max_tokens` borne aussi un appel d'outil : un `write_file` de long document (ou le raisonnement d'un modèle « thinking » plus l'appel) peut l'atteindre, et l'appel est alors coupé. mm le rend deux fois au modèle en lui demandant de procéder par morceaux, puis s'arrête avec `the model hit max_tokens (N) while writing a tool call` ; augmentez la valeur si cela arrive. En streaming, mm demande aussi au serveur le compte des tokens (`stream_options.include_usage`). |
26 | `watchdogTimeout` | durée | `20s` | Silence toléré entre deux tokens diffusés avant de déclarer la connexion bloquée. Doublé pour les requêtes de résumé. |26 | `watchdogTimeout` | durée | `20s` | Silence toléré entre deux tokens diffusés avant de déclarer la connexion bloquée. Doublé pour les requêtes de résumé. |
27 | `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama n'envoie pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). |27 | `firstTokenTimeout` | durée | `3m` | Silence toléré avant le premier token d'un appel au modèle, pendant que le serveur lit le prompt (chaque aller-retour d'un tour en a un). Jamais plus court que `watchdogTimeout` ; doit être >= 0. Avec un serveur llama.cpp (`dmr`, `llamacpp`, `llmman` — dont les modèles GGUF locaux tournent dans llama-server), mm demande `return_progress` : chaque rapport d'avancement relance ce délai, et le spinner affiche `Reading the prompt 12.0k/25.3k tokens`. Ollama n'envoie pas d'avancement : cette seule attente couvre tout le prompt (et le chargement du modèle). À l'expiration, le watchdog dit `no first token from the server after …` (avant le premier token) ou `connection hang detected after …` (après). |
28+| `bashTimeout` | durée | `30s` | Durée maximale d'une commande `bash` avant qu'elle soit tuée ; l'outil répond alors `[error: <bashTimeout> timeout exceeded]`. Doit être > 0. Le watchdog continue de compter pendant qu'une commande tourne (ticket 57) : une commande plus longue que `watchdogTimeout` termine le tour, donc relevez les deux ensemble. |
28 | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. |29 | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. |
29 | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. |30 | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. |
30 | `acp` | dictionnaire | voir ci-dessous | La façon dont `mm -acp` se présente à l'éditeur. |31 | `acp` | dictionnaire | voir ci-dessous | La façon dont `mm -acp` se présente à l'éditeur. |
@@ -118,6 +119,7 @@ Signalées sous la forme `[config error: <chemin>: <message>]`, code de sortie 1
118 | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | une extension sans son point |119 | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | une extension sans son point |
119 | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | deux serveurs sur une même extension |120 | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | deux serveurs sur une même extension |
120 | `permissions must be "auto" or "ask"` | toute autre valeur |121 | `permissions must be "auto" or "ask"` | toute autre valeur |
122+| `bashTimeout must be > 0` | zéro ou négative |
121 | `mmConfigs: "Bob" is not a valid name (…)` | un nom avec des majuscules, des espaces ou d'autres caractères, ou commençant par `-` |123 | `mmConfigs: "Bob" is not a valid name (…)` | un nom avec des majuscules, des espaces ou d'autres caractères, ou commençant par `-` |
122 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` |124 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` |
123 | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""` |125 | `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""` |
added internal/ask/cli.go +134 -0
new file mode 100644
@@ -0,0 +1,134 @@
1+package ask
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "io"
8+ "sort"
9+ "strings"
10+
11+ "mm/internal/config"
12+ "mm/internal/ui"
13+)
14+
15+// The command-line side of /ask: `mm --ask <agent>` and
16+// `mm --ask-with-context <agent>`, for a skill, a script or a prompt that can
17+// only run a shell command. Same machinery as the slash commands — a child
18+// `mm -acp`, the same environment fence and depth limit — but the answer goes
19+// to stdout and NOTHING else does: everything human (the label, the other
20+// agent's tool calls, errors) goes to stderr, so `answer=$(mm --ask lucy -p …)`
21+// holds the answer alone.
22+//
23+// mm --ask lucy -p "What is the capital of Peru?"
24+// echo "What is the capital of Peru?" | mm --ask lucy
25+// printf 'User: my name is Ann\n' | mm --ask-with-context lucy -p "What is my name?"
26+// printf 'User: my name is Ann\n---\nWhat is my name?\n' | mm --ask-with-context lucy
27+
28+// CLIRequest is one command-line ask.
29+type CLIRequest struct {
30+ Agent string // a key of mmConfigs
31+ WithContext bool
32+ Question string
33+ Context string // what /ask-with-context would send as the conversation
34+}
35+
36+// contextSeparator splits stdin into context and question when the question
37+// is not given with -p.
38+const contextSeparator = "\n---\n"
39+
40+// ReadCLIRequest builds the request from the flags and stdin. The question is
41+// -p when given, stdin otherwise. With context, stdin is the context; when -p
42+// is empty, stdin is "<context>\n---\n<question>".
43+func ReadCLIRequest(agent string, withContext bool, prompt string, stdin io.Reader) (CLIRequest, error) {
44+ req := CLIRequest{Agent: agent, WithContext: withContext, Question: strings.TrimSpace(prompt)}
45+ needStdin := withContext || req.Question == ""
46+ input := ""
47+ if needStdin && stdin != nil {
48+ data, err := io.ReadAll(stdin)
49+ if err != nil {
50+ return req, fmt.Errorf("reading stdin: %w", err)
51+ }
52+ input = string(data)
53+ }
54+ switch {
55+ case withContext && req.Question == "":
56+ ctxText, question, found := strings.Cut(input, contextSeparator)
57+ if !found {
58+ return req, errors.New(`--ask-with-context: give the question with -p, or end stdin with a "---" line followed by the question`)
59+ }
60+ req.Context, req.Question = strings.TrimSpace(ctxText), strings.TrimSpace(question)
61+ case withContext:
62+ req.Context = strings.TrimSpace(input)
63+ default:
64+ if req.Question == "" {
65+ req.Question = strings.TrimSpace(input)
66+ }
67+ }
68+ if req.Question == "" {
69+ return req, errors.New("nothing to ask: give the question with -p or on stdin")
70+ }
71+ if withContext && req.Context == "" {
72+ return req, errors.New("--ask-with-context: no context on stdin")
73+ }
74+ return req, nil
75+}
76+
77+// CLI runs a command-line ask: the answer to stdout, the rest to stderr.
78+// The other agent's tool calls are allowed, as in the line mode: the call to
79+// `mm --ask` is itself the command a person (or a permission prompt) let run.
80+func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stderr io.Writer) error {
81+ path, ok := config.Cfg.MMConfigs[req.Agent]
82+ if !ok {
83+ return fmt.Errorf("unknown agent %q — agents in this config: %s", req.Agent, agentList())
84+ }
85+ label := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(path))
86+ if req.WithContext {
87+ label += ", with the conversation"
88+ }
89+ fmt.Fprintln(stderr, label+"…")
90+ sink := &cliSink{out: stdout, log: stderr}
91+ answer, err := p.Ask(ctx, req.Agent, path, cwd, Prompt(req.Question, req.Context), sink)
92+ if answer != "" && !strings.HasSuffix(answer, "\n") {
93+ fmt.Fprintln(stdout)
94+ }
95+ if err == nil && answer == "" {
96+ return errors.New(req.Agent + " gave no answer")
97+ }
98+ return err
99+}
100+
101+// agentList is the configured agents, for an error message.
102+func agentList() string {
103+ names := make([]string, 0, len(config.Cfg.MMConfigs))
104+ for name := range config.Cfg.MMConfigs {
105+ names = append(names, name)
106+ }
107+ if len(names) == 0 {
108+ return "none (no mmConfigs — pass the asking agent's config with -config, or set MM_CONFIG)"
109+ }
110+ sort.Strings(names)
111+ return strings.Join(names, ", ")
112+}
113+
114+// cliSink: the answer on stdout, one stderr line per tool call.
115+type cliSink struct {
116+ out io.Writer
117+ log io.Writer
118+}
119+
120+var _ ui.Sink = (*cliSink)(nil)
121+
122+func (s *cliSink) Text(text string) { fmt.Fprint(s.out, text) }
123+func (s *cliSink) Reasoning(string) {}
124+func (s *cliSink) ToolStart(ev ui.ToolEvent) {
125+ fmt.Fprintln(s.log, "🛠️ "+ev.Title)
126+}
127+func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) {
128+ if res.Failed {
129+ fmt.Fprintln(s.log, " │ failed")
130+ }
131+}
132+func (s *cliSink) Allow(context.Context, ui.ToolEvent) bool { return true }
133+func (s *cliSink) ToolRunning(string) {}
134+func (s *cliSink) WorkDir() string { return "" }
new file mode 100644
@@ -0,0 +1,134 @@
1+package ask
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "io"
8+ "sort"
9+ "strings"
10+
11+ "mm/internal/config"
12+ "mm/internal/ui"
13+)
14+
15+// The command-line side of /ask: `mm --ask <agent>` and
16+// `mm --ask-with-context <agent>`, for a skill, a script or a prompt that can
17+// only run a shell command. Same machinery as the slash commands — a child
18+// `mm -acp`, the same environment fence and depth limit — but the answer goes
19+// to stdout and NOTHING else does: everything human (the label, the other
20+// agent's tool calls, errors) goes to stderr, so `answer=$(mm --ask lucy -p …)`
21+// holds the answer alone.
22+//
23+// mm --ask lucy -p "What is the capital of Peru?"
24+// echo "What is the capital of Peru?" | mm --ask lucy
25+// printf 'User: my name is Ann\n' | mm --ask-with-context lucy -p "What is my name?"
26+// printf 'User: my name is Ann\n---\nWhat is my name?\n' | mm --ask-with-context lucy
27+
28+// CLIRequest is one command-line ask.
29+type CLIRequest struct {
30+ Agent string // a key of mmConfigs
31+ WithContext bool
32+ Question string
33+ Context string // what /ask-with-context would send as the conversation
34+}
35+
36+// contextSeparator splits stdin into context and question when the question
37+// is not given with -p.
38+const contextSeparator = "\n---\n"
39+
40+// ReadCLIRequest builds the request from the flags and stdin. The question is
41+// -p when given, stdin otherwise. With context, stdin is the context; when -p
42+// is empty, stdin is "<context>\n---\n<question>".
43+func ReadCLIRequest(agent string, withContext bool, prompt string, stdin io.Reader) (CLIRequest, error) {
44+ req := CLIRequest{Agent: agent, WithContext: withContext, Question: strings.TrimSpace(prompt)}
45+ needStdin := withContext || req.Question == ""
46+ input := ""
47+ if needStdin && stdin != nil {
48+ data, err := io.ReadAll(stdin)
49+ if err != nil {
50+ return req, fmt.Errorf("reading stdin: %w", err)
51+ }
52+ input = string(data)
53+ }
54+ switch {
55+ case withContext && req.Question == "":
56+ ctxText, question, found := strings.Cut(input, contextSeparator)
57+ if !found {
58+ return req, errors.New(`--ask-with-context: give the question with -p, or end stdin with a "---" line followed by the question`)
59+ }
60+ req.Context, req.Question = strings.TrimSpace(ctxText), strings.TrimSpace(question)
61+ case withContext:
62+ req.Context = strings.TrimSpace(input)
63+ default:
64+ if req.Question == "" {
65+ req.Question = strings.TrimSpace(input)
66+ }
67+ }
68+ if req.Question == "" {
69+ return req, errors.New("nothing to ask: give the question with -p or on stdin")
70+ }
71+ if withContext && req.Context == "" {
72+ return req, errors.New("--ask-with-context: no context on stdin")
73+ }
74+ return req, nil
75+}
76+
77+// CLI runs a command-line ask: the answer to stdout, the rest to stderr.
78+// The other agent's tool calls are allowed, as in the line mode: the call to
79+// `mm --ask` is itself the command a person (or a permission prompt) let run.
80+func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stderr io.Writer) error {
81+ path, ok := config.Cfg.MMConfigs[req.Agent]
82+ if !ok {
83+ return fmt.Errorf("unknown agent %q — agents in this config: %s", req.Agent, agentList())
84+ }
85+ label := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(path))
86+ if req.WithContext {
87+ label += ", with the conversation"
88+ }
89+ fmt.Fprintln(stderr, label+"…")
90+ sink := &cliSink{out: stdout, log: stderr}
91+ answer, err := p.Ask(ctx, req.Agent, path, cwd, Prompt(req.Question, req.Context), sink)
92+ if answer != "" && !strings.HasSuffix(answer, "\n") {
93+ fmt.Fprintln(stdout)
94+ }
95+ if err == nil && answer == "" {
96+ return errors.New(req.Agent + " gave no answer")
97+ }
98+ return err
99+}
100+
101+// agentList is the configured agents, for an error message.
102+func agentList() string {
103+ names := make([]string, 0, len(config.Cfg.MMConfigs))
104+ for name := range config.Cfg.MMConfigs {
105+ names = append(names, name)
106+ }
107+ if len(names) == 0 {
108+ return "none (no mmConfigs — pass the asking agent's config with -config, or set MM_CONFIG)"
109+ }
110+ sort.Strings(names)
111+ return strings.Join(names, ", ")
112+}
113+
114+// cliSink: the answer on stdout, one stderr line per tool call.
115+type cliSink struct {
116+ out io.Writer
117+ log io.Writer
118+}
119+
120+var _ ui.Sink = (*cliSink)(nil)
121+
122+func (s *cliSink) Text(text string) { fmt.Fprint(s.out, text) }
123+func (s *cliSink) Reasoning(string) {}
124+func (s *cliSink) ToolStart(ev ui.ToolEvent) {
125+ fmt.Fprintln(s.log, "🛠️ "+ev.Title)
126+}
127+func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) {
128+ if res.Failed {
129+ fmt.Fprintln(s.log, " │ failed")
130+ }
131+}
132+func (s *cliSink) Allow(context.Context, ui.ToolEvent) bool { return true }
133+func (s *cliSink) ToolRunning(string) {}
134+func (s *cliSink) WorkDir() string { return "" }
added internal/ask/cli_test.go +85 -0
new file mode 100644
@@ -0,0 +1,85 @@
1+package ask
2+
3+import (
4+ "bytes"
5+ "context"
6+ "strings"
7+ "testing"
8+)
9+
10+// The question comes from -p or stdin; with context, stdin is the context, or
11+// "context --- question" when -p is empty. Every missing piece is an error
12+// saying what to give.
13+func TestReadCLIRequest(t *testing.T) {
14+ for _, tc := range []struct {
15+ name string
16+ withContext bool
17+ prompt string
18+ stdin string
19+ want CLIRequest
20+ wantErr string
21+ }{
22+ {"question with -p", false, "capital of Peru?", "", CLIRequest{Agent: "lucy", Question: "capital of Peru?"}, ""},
23+ {"question on stdin", false, "", "capital of Peru?\n", CLIRequest{Agent: "lucy", Question: "capital of Peru?"}, ""},
24+ {"-p wins over stdin", false, "from -p", "from stdin", CLIRequest{Agent: "lucy", Question: "from -p"}, ""},
25+ {"no question", false, "", " \n", CLIRequest{}, "nothing to ask"},
26+ {"context on stdin, question with -p", true, "my name?", "User: I am Ann\n", CLIRequest{Agent: "lucy", WithContext: true, Question: "my name?", Context: "User: I am Ann"}, ""},
27+ {"context and question on stdin", true, "", "User: I am Ann\n---\nmy name?\n", CLIRequest{Agent: "lucy", WithContext: true, Question: "my name?", Context: "User: I am Ann"}, ""},
28+ {"context without separator", true, "", "User: I am Ann\n", CLIRequest{}, `end stdin with a "---" line`},
29+ {"no context", true, "my name?", "", CLIRequest{}, "no context on stdin"},
30+ } {
31+ got, err := ReadCLIRequest("lucy", tc.withContext, tc.prompt, strings.NewReader(tc.stdin))
32+ if tc.wantErr != "" {
33+ if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
34+ t.Errorf("%s: err %v, want %q", tc.name, err, tc.wantErr)
35+ }
36+ continue
37+ }
38+ if err != nil || got != tc.want {
39+ t.Errorf("%s: %+v, %v — want %+v", tc.name, got, err, tc.want)
40+ }
41+ }
42+}
43+
44+// stdout holds the answer and nothing else — the label and the tool calls go
45+// to stderr — so `answer=$(mm --ask bob …)` is the answer alone.
46+func TestCLIPrintsOnlyTheAnswerOnStdout(t *testing.T) {
47+ withAgent(t)
48+ var out, errs bytes.Buffer
49+ err := fakePool(t, "tool").CLI(context.Background(), CLIRequest{Agent: "bob", Question: "2+2?"}, t.TempDir(), &out, &errs)
50+ if err != nil {
51+ t.Fatal(err)
52+ }
53+ if got := out.String(); got != "You asked: 2+2? (tool allowed)\n" {
54+ t.Errorf("stdout = %q", got)
55+ }
56+ for _, want := range []string{"🗣 asking bob (ollama/qwen3.5:2b)…", "🛠️ bob · bash: ls"} {
57+ if !strings.Contains(errs.String(), want) {
58+ t.Errorf("stderr lacks %q:\n%s", want, errs.String())
59+ }
60+ }
61+}
62+
63+// With context, the other agent receives the transcript first, in the
64+// /ask-with-context format.
65+func TestCLIWithContext(t *testing.T) {
66+ withAgent(t)
67+ var out, errs bytes.Buffer
68+ req := CLIRequest{Agent: "bob", WithContext: true, Question: "my name?", Context: "User: I am Ann"}
69+ if err := fakePool(t, "echo").CLI(context.Background(), req, t.TempDir(), &out, &errs); err != nil {
70+ t.Fatal(err)
71+ }
72+ if got := out.String(); !strings.Contains(got, "User: I am Ann\n\n[Question]\nmy name?") || !strings.Contains(errs.String(), "with the conversation") {
73+ t.Errorf("stdout %q, stderr %q", got, errs.String())
74+ }
75+}
76+
77+// An unknown agent names the configured ones; with none, it says how to give
78+// the asking agent's config.
79+func TestCLIUnknownAgent(t *testing.T) {
80+ withAgent(t)
81+ err := fakePool(t, "echo").CLI(context.Background(), CLIRequest{Agent: "spock", Question: "q"}, t.TempDir(), &bytes.Buffer{}, &bytes.Buffer{})
82+ if err == nil || !strings.Contains(err.Error(), `unknown agent "spock" — agents in this config: bob`) {
83+ t.Errorf("err = %v", err)
84+ }
85+}
new file mode 100644
@@ -0,0 +1,85 @@
1+package ask
2+
3+import (
4+ "bytes"
5+ "context"
6+ "strings"
7+ "testing"
8+)
9+
10+// The question comes from -p or stdin; with context, stdin is the context, or
11+// "context --- question" when -p is empty. Every missing piece is an error
12+// saying what to give.
13+func TestReadCLIRequest(t *testing.T) {
14+ for _, tc := range []struct {
15+ name string
16+ withContext bool
17+ prompt string
18+ stdin string
19+ want CLIRequest
20+ wantErr string
21+ }{
22+ {"question with -p", false, "capital of Peru?", "", CLIRequest{Agent: "lucy", Question: "capital of Peru?"}, ""},
23+ {"question on stdin", false, "", "capital of Peru?\n", CLIRequest{Agent: "lucy", Question: "capital of Peru?"}, ""},
24+ {"-p wins over stdin", false, "from -p", "from stdin", CLIRequest{Agent: "lucy", Question: "from -p"}, ""},
25+ {"no question", false, "", " \n", CLIRequest{}, "nothing to ask"},
26+ {"context on stdin, question with -p", true, "my name?", "User: I am Ann\n", CLIRequest{Agent: "lucy", WithContext: true, Question: "my name?", Context: "User: I am Ann"}, ""},
27+ {"context and question on stdin", true, "", "User: I am Ann\n---\nmy name?\n", CLIRequest{Agent: "lucy", WithContext: true, Question: "my name?", Context: "User: I am Ann"}, ""},
28+ {"context without separator", true, "", "User: I am Ann\n", CLIRequest{}, `end stdin with a "---" line`},
29+ {"no context", true, "my name?", "", CLIRequest{}, "no context on stdin"},
30+ } {
31+ got, err := ReadCLIRequest("lucy", tc.withContext, tc.prompt, strings.NewReader(tc.stdin))
32+ if tc.wantErr != "" {
33+ if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
34+ t.Errorf("%s: err %v, want %q", tc.name, err, tc.wantErr)
35+ }
36+ continue
37+ }
38+ if err != nil || got != tc.want {
39+ t.Errorf("%s: %+v, %v — want %+v", tc.name, got, err, tc.want)
40+ }
41+ }
42+}
43+
44+// stdout holds the answer and nothing else — the label and the tool calls go
45+// to stderr — so `answer=$(mm --ask bob …)` is the answer alone.
46+func TestCLIPrintsOnlyTheAnswerOnStdout(t *testing.T) {
47+ withAgent(t)
48+ var out, errs bytes.Buffer
49+ err := fakePool(t, "tool").CLI(context.Background(), CLIRequest{Agent: "bob", Question: "2+2?"}, t.TempDir(), &out, &errs)
50+ if err != nil {
51+ t.Fatal(err)
52+ }
53+ if got := out.String(); got != "You asked: 2+2? (tool allowed)\n" {
54+ t.Errorf("stdout = %q", got)
55+ }
56+ for _, want := range []string{"🗣 asking bob (ollama/qwen3.5:2b)…", "🛠️ bob · bash: ls"} {
57+ if !strings.Contains(errs.String(), want) {
58+ t.Errorf("stderr lacks %q:\n%s", want, errs.String())
59+ }
60+ }
61+}
62+
63+// With context, the other agent receives the transcript first, in the
64+// /ask-with-context format.
65+func TestCLIWithContext(t *testing.T) {
66+ withAgent(t)
67+ var out, errs bytes.Buffer
68+ req := CLIRequest{Agent: "bob", WithContext: true, Question: "my name?", Context: "User: I am Ann"}
69+ if err := fakePool(t, "echo").CLI(context.Background(), req, t.TempDir(), &out, &errs); err != nil {
70+ t.Fatal(err)
71+ }
72+ if got := out.String(); !strings.Contains(got, "User: I am Ann\n\n[Question]\nmy name?") || !strings.Contains(errs.String(), "with the conversation") {
73+ t.Errorf("stdout %q, stderr %q", got, errs.String())
74+ }
75+}
76+
77+// An unknown agent names the configured ones; with none, it says how to give
78+// the asking agent's config.
79+func TestCLIUnknownAgent(t *testing.T) {
80+ withAgent(t)
81+ err := fakePool(t, "echo").CLI(context.Background(), CLIRequest{Agent: "spock", Question: "q"}, t.TempDir(), &bytes.Buffer{}, &bytes.Buffer{})
82+ if err == nil || !strings.Contains(err.Error(), `unknown agent "spock" — agents in this config: bob`) {
83+ t.Errorf("err = %v", err)
84+ }
85+}
added internal/ask/orphan.go +39 -0
new file mode 100644
@@ -0,0 +1,39 @@
1+package ask
2+
3+import (
4+ "context"
5+ "os"
6+ "time"
7+)
8+
9+// getppid is os.Getppid, swapped in tests.
10+var getppid = os.Getppid
11+
12+// WatchParent calls cancel as soon as this process's parent is gone — the
13+// operating system then hands it to another parent, so its parent pid
14+// changes. For `mm --ask` run by a shell that is killed: the bash tool of the
15+// asking agent kills `bash` when its budget runs out or its turn is
16+// cancelled, not bash's children, and without this the ask — and the agent
17+// it started — kept running, orphaned, writing files nobody asked for any
18+// more (seen 2026-10-02). Stops watching when ctx ends.
19+//
20+// ctx, cancel := context.WithCancel(ctx)
21+// ask.WatchParent(ctx, cancel, 500*time.Millisecond)
22+func WatchParent(ctx context.Context, cancel context.CancelFunc, every time.Duration) {
23+ parent := getppid()
24+ go func() {
25+ tick := time.NewTicker(every)
26+ defer tick.Stop()
27+ for {
28+ select {
29+ case <-ctx.Done():
30+ return
31+ case <-tick.C:
32+ if getppid() != parent {
33+ cancel()
34+ return
35+ }
36+ }
37+ }
38+ }()
39+}
new file mode 100644
@@ -0,0 +1,39 @@
1+package ask
2+
3+import (
4+ "context"
5+ "os"
6+ "time"
7+)
8+
9+// getppid is os.Getppid, swapped in tests.
10+var getppid = os.Getppid
11+
12+// WatchParent calls cancel as soon as this process's parent is gone — the
13+// operating system then hands it to another parent, so its parent pid
14+// changes. For `mm --ask` run by a shell that is killed: the bash tool of the
15+// asking agent kills `bash` when its budget runs out or its turn is
16+// cancelled, not bash's children, and without this the ask — and the agent
17+// it started — kept running, orphaned, writing files nobody asked for any
18+// more (seen 2026-10-02). Stops watching when ctx ends.
19+//
20+// ctx, cancel := context.WithCancel(ctx)
21+// ask.WatchParent(ctx, cancel, 500*time.Millisecond)
22+func WatchParent(ctx context.Context, cancel context.CancelFunc, every time.Duration) {
23+ parent := getppid()
24+ go func() {
25+ tick := time.NewTicker(every)
26+ defer tick.Stop()
27+ for {
28+ select {
29+ case <-ctx.Done():
30+ return
31+ case <-tick.C:
32+ if getppid() != parent {
33+ cancel()
34+ return
35+ }
36+ }
37+ }
38+ }()
39+}
added internal/ask/orphan_test.go +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+package ask
2+
3+import (
4+ "context"
5+ "sync/atomic"
6+ "testing"
7+ "time"
8+)
9+
10+// When the parent pid changes — the parent died — the context is cancelled;
11+// while it stays the same, nothing happens.
12+func TestWatchParentCancelsWhenOrphaned(t *testing.T) {
13+ var ppid atomic.Int64
14+ ppid.Store(100)
15+ saved := getppid
16+ getppid = func() int { return int(ppid.Load()) }
17+ t.Cleanup(func() { getppid = saved })
18+
19+ ctx, cancel := context.WithCancel(context.Background())
20+ defer cancel()
21+ WatchParent(ctx, cancel, 5*time.Millisecond)
22+
23+ time.Sleep(30 * time.Millisecond)
24+ if ctx.Err() != nil {
25+ t.Fatal("cancelled while the parent is alive")
26+ }
27+ ppid.Store(1) // re-parented to init
28+ select {
29+ case <-ctx.Done():
30+ case <-time.After(2 * time.Second):
31+ t.Fatal("not cancelled after the parent died")
32+ }
33+}
new file mode 100644
@@ -0,0 +1,33 @@
1+package ask
2+
3+import (
4+ "context"
5+ "sync/atomic"
6+ "testing"
7+ "time"
8+)
9+
10+// When the parent pid changes — the parent died — the context is cancelled;
11+// while it stays the same, nothing happens.
12+func TestWatchParentCancelsWhenOrphaned(t *testing.T) {
13+ var ppid atomic.Int64
14+ ppid.Store(100)
15+ saved := getppid
16+ getppid = func() int { return int(ppid.Load()) }
17+ t.Cleanup(func() { getppid = saved })
18+
19+ ctx, cancel := context.WithCancel(context.Background())
20+ defer cancel()
21+ WatchParent(ctx, cancel, 5*time.Millisecond)
22+
23+ time.Sleep(30 * time.Millisecond)
24+ if ctx.Err() != nil {
25+ t.Fatal("cancelled while the parent is alive")
26+ }
27+ ppid.Store(1) // re-parented to init
28+ select {
29+ case <-ctx.Done():
30+ case <-time.After(2 * time.Second):
31+ t.Fatal("not cancelled after the parent died")
32+ }
33+}
modified internal/config/agents_test.go +31 -0
@@ -94,3 +94,34 @@ func TestSessionsEnvTurnsPersistenceOff(t *testing.T) {
9494 t.Error("AGENT_SESSIONS unset: the file's sessions.enabled was ignored")
9595 }
9696 }
97+
98+// bashTimeout defaults to the former hard-coded 30s, is read from the file,
99+// and must be positive.
100+func TestBashTimeoutConfig(t *testing.T) {
101+ defaults := Cfg
102+ t.Cleanup(func() { Cfg = defaults })
103+ if Cfg.BashTimeout.String() != "30s" {
104+ t.Errorf("default bashTimeout = %v, want 30s", Cfg.BashTimeout)
105+ }
106+ dir := t.TempDir()
107+ for _, tc := range []struct{ yaml, want, wantErr string }{
108+ {"bashTimeout: 3m\n", "3m0s", ""},
109+ {"bashTimeout: 0s\n", "", "bashTimeout must be > 0"},
110+ } {
111+ Cfg = defaults
112+ path := filepath.Join(dir, "a.yaml")
113+ if err := os.WriteFile(path, []byte(tc.yaml), 0o644); err != nil {
114+ t.Fatal(err)
115+ }
116+ _, err := Load(path)
117+ if tc.wantErr != "" {
118+ if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
119+ t.Errorf("%q: err %v", tc.yaml, err)
120+ }
121+ continue
122+ }
123+ if err != nil || Cfg.BashTimeout.String() != tc.want {
124+ t.Errorf("%q: %v, %v", tc.yaml, Cfg.BashTimeout, err)
125+ }
126+ }
127+}
@@ -94,3 +94,34 @@ func TestSessionsEnvTurnsPersistenceOff(t *testing.T) {
94 t.Error("AGENT_SESSIONS unset: the file's sessions.enabled was ignored")94 t.Error("AGENT_SESSIONS unset: the file's sessions.enabled was ignored")
95 }95 }
96 }96 }
97+
98+// bashTimeout defaults to the former hard-coded 30s, is read from the file,
99+// and must be positive.
100+func TestBashTimeoutConfig(t *testing.T) {
101+ defaults := Cfg
102+ t.Cleanup(func() { Cfg = defaults })
103+ if Cfg.BashTimeout.String() != "30s" {
104+ t.Errorf("default bashTimeout = %v, want 30s", Cfg.BashTimeout)
105+ }
106+ dir := t.TempDir()
107+ for _, tc := range []struct{ yaml, want, wantErr string }{
108+ {"bashTimeout: 3m\n", "3m0s", ""},
109+ {"bashTimeout: 0s\n", "", "bashTimeout must be > 0"},
110+ } {
111+ Cfg = defaults
112+ path := filepath.Join(dir, "a.yaml")
113+ if err := os.WriteFile(path, []byte(tc.yaml), 0o644); err != nil {
114+ t.Fatal(err)
115+ }
116+ _, err := Load(path)
117+ if tc.wantErr != "" {
118+ if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
119+ t.Errorf("%q: err %v", tc.yaml, err)
120+ }
121+ continue
122+ }
123+ if err != nil || Cfg.BashTimeout.String() != tc.want {
124+ t.Errorf("%q: %v, %v", tc.yaml, Cfg.BashTimeout, err)
125+ }
126+ }
127+}
modified internal/config/config.go +10 -0
@@ -119,6 +119,13 @@ type Config struct {
119119 // Never shorter than WatchdogTimeout.
120120 FirstTokenTimeout time.Duration `yaml:"firstTokenTimeout"`
121121
122+ // BashTimeout: how long one `bash` command may run before it is killed
123+ // (default 30s). Raise it for commands that legitimately take long — a
124+ // skill calling another agent with `mm --ask`, a slow build. While a
125+ // command runs, the watchdog keeps counting (ticket 57): raise
126+ // watchdogTimeout with it.
127+ BashTimeout time.Duration `yaml:"bashTimeout"`
128+
122129 // Context: when and how the conversation history is compressed
123130 // (internal/compact). Off unless `context.enabled: true`. The window it
124131 // measures against is the top-level ContextWindow above — or, when that is
@@ -456,6 +463,7 @@ which ones you left running.`,
456463 },
457464 WatchdogTimeout: 20 * time.Second,
458465 FirstTokenTimeout: 3 * time.Minute,
466+ BashTimeout: 30 * time.Second,
459467 Sessions: SessionsConfig{
460468 Enabled: false,
461469 Dir: ".mm/sessions",
@@ -605,6 +613,8 @@ func (c Config) validate() error {
605613 return errors.New("contextWindow must be >= 0")
606614 case c.FirstTokenTimeout < 0:
607615 return errors.New("firstTokenTimeout must be >= 0")
616+ case c.BashTimeout <= 0:
617+ return errors.New("bashTimeout must be > 0")
608618 case c.MaxOutput <= 0:
609619 return errors.New("maxOutput must be > 0")
610620 case c.MaxTurns <= 0:
@@ -119,6 +119,13 @@ type Config struct {
119 // Never shorter than WatchdogTimeout.119 // Never shorter than WatchdogTimeout.
120 FirstTokenTimeout time.Duration `yaml:"firstTokenTimeout"`120 FirstTokenTimeout time.Duration `yaml:"firstTokenTimeout"`
121 121
122+ // BashTimeout: how long one `bash` command may run before it is killed
123+ // (default 30s). Raise it for commands that legitimately take long — a
124+ // skill calling another agent with `mm --ask`, a slow build. While a
125+ // command runs, the watchdog keeps counting (ticket 57): raise
126+ // watchdogTimeout with it.
127+ BashTimeout time.Duration `yaml:"bashTimeout"`
128+
122 // Context: when and how the conversation history is compressed129 // Context: when and how the conversation history is compressed
123 // (internal/compact). Off unless `context.enabled: true`. The window it130 // (internal/compact). Off unless `context.enabled: true`. The window it
124 // measures against is the top-level ContextWindow above — or, when that is131 // measures against is the top-level ContextWindow above — or, when that is
@@ -456,6 +463,7 @@ which ones you left running.`,
456 },463 },
457 WatchdogTimeout: 20 * time.Second,464 WatchdogTimeout: 20 * time.Second,
458 FirstTokenTimeout: 3 * time.Minute,465 FirstTokenTimeout: 3 * time.Minute,
466+ BashTimeout: 30 * time.Second,
459 Sessions: SessionsConfig{467 Sessions: SessionsConfig{
460 Enabled: false,468 Enabled: false,
461 Dir: ".mm/sessions",469 Dir: ".mm/sessions",
@@ -605,6 +613,8 @@ func (c Config) validate() error {
605 return errors.New("contextWindow must be >= 0")613 return errors.New("contextWindow must be >= 0")
606 case c.FirstTokenTimeout < 0:614 case c.FirstTokenTimeout < 0:
607 return errors.New("firstTokenTimeout must be >= 0")615 return errors.New("firstTokenTimeout must be >= 0")
616+ case c.BashTimeout <= 0:
617+ return errors.New("bashTimeout must be > 0")
608 case c.MaxOutput <= 0:618 case c.MaxOutput <= 0:
609 return errors.New("maxOutput must be > 0")619 return errors.New("maxOutput must be > 0")
610 case c.MaxTurns <= 0:620 case c.MaxTurns <= 0:
modified internal/tools/bash_test.go +16 -0
@@ -88,3 +88,19 @@ func TestBashReportsOutputAndExitCode(t *testing.T) {
8888 t.Fatalf("a failing command was not reported: failed=%v out=%q", failed, out)
8989 }
9090 }
91+
92+// bashTimeout is the budget of one command, and the error names the value
93+// that was exceeded, not a hard-coded 30s.
94+func TestBashTimeoutIsConfigurable(t *testing.T) {
95+ saved := config.Cfg.BashTimeout
96+ t.Cleanup(func() { config.Cfg.BashTimeout = saved })
97+ config.Cfg.BashTimeout = 300 * time.Millisecond
98+ start := time.Now()
99+ out, failed := runBash(context.Background(), "", "sleep 5")
100+ if !failed || !strings.Contains(out, "[error: 300ms timeout exceeded]") {
101+ t.Errorf("out %q, failed %v", out, failed)
102+ }
103+ if took := time.Since(start); took > 4*time.Second {
104+ t.Errorf("took %v: the 300ms budget was not applied", took)
105+ }
106+}
@@ -88,3 +88,19 @@ func TestBashReportsOutputAndExitCode(t *testing.T) {
88 t.Fatalf("a failing command was not reported: failed=%v out=%q", failed, out)88 t.Fatalf("a failing command was not reported: failed=%v out=%q", failed, out)
89 }89 }
90 }90 }
91+
92+// bashTimeout is the budget of one command, and the error names the value
93+// that was exceeded, not a hard-coded 30s.
94+func TestBashTimeoutIsConfigurable(t *testing.T) {
95+ saved := config.Cfg.BashTimeout
96+ t.Cleanup(func() { config.Cfg.BashTimeout = saved })
97+ config.Cfg.BashTimeout = 300 * time.Millisecond
98+ start := time.Now()
99+ out, failed := runBash(context.Background(), "", "sleep 5")
100+ if !failed || !strings.Contains(out, "[error: 300ms timeout exceeded]") {
101+ t.Errorf("out %q, failed %v", out, failed)
102+ }
103+ if took := time.Since(start); took > 4*time.Second {
104+ t.Errorf("took %v: the 300ms budget was not applied", took)
105+ }
106+}
modified internal/tools/tools.go +7 -9
@@ -105,12 +105,9 @@ func echoOutput(out string) {
105105 printMu.Unlock()
106106 }
107107
108-// bashBudget is how long one command may run; waitDelay bounds the wait for a
109-// process that will not die once the budget or the turn is over.
110-const (
111- bashBudget = 30 * time.Second
112- waitDelay = 2 * time.Second
113-)
108+// waitDelay bounds the wait for a process that will not die once its budget
109+// (config bashTimeout, 30s by default) or the turn is over.
110+const waitDelay = 2 * time.Second
114111
115112 // outputCap is how much of a command's output is read back from disk, per end.
116113 // truncate() keeps a head and a tail, so those are what we read: a command
@@ -125,7 +122,7 @@ const outputCap = 1 << 20
125122 //
126123 // Two things here are not obvious, and both are ticket 26.
127124 //
128-// The command runs under the TURN's context, with 30s of its own as a child of
125+// The command runs under the TURN's context, with bashTimeout of its own as a child of
129126 // it: cancelling the turn — Escape, Ctrl-C, /abort, or asking something else —
130127 // now kills the command. It used to hang off context.Background(), so a
131128 // cancellation never reached the command that was actually running.
@@ -144,7 +141,8 @@ const outputCap = 1 << 20
144141 // descriptor and keeps working, which is the point — the user asked for that
145142 // server — and writes into a file nobody reads.
146143 func runBash(ctx context.Context, dir, command string) (output string, failed bool) {
147- ctx, cancel := context.WithTimeout(ctx, bashBudget)
144+ budget := config.Cfg.BashTimeout
145+ ctx, cancel := context.WithTimeout(ctx, budget)
148146 defer cancel()
149147
150148 sink, err := os.CreateTemp("", "mm-bash-*")
@@ -167,7 +165,7 @@ func runBash(ctx context.Context, dir, command string) (output string, failed bo
167165
168166 switch {
169167 case errors.Is(ctx.Err(), context.DeadlineExceeded):
170- return output + "\n[error: 30s timeout exceeded]", true
168+ return output + fmt.Sprintf("\n[error: %s timeout exceeded]", budget), true
171169 case errors.Is(ctx.Err(), context.Canceled):
172170 return output + "\n[error: cancelled]", true
173171 case err != nil:
@@ -105,12 +105,9 @@ func echoOutput(out string) {
105 printMu.Unlock()105 printMu.Unlock()
106 }106 }
107 107
108-// bashBudget is how long one command may run; waitDelay bounds the wait for a108+// waitDelay bounds the wait for a process that will not die once its budget
109-// process that will not die once the budget or the turn is over.109+// (config bashTimeout, 30s by default) or the turn is over.
110-const (110+const waitDelay = 2 * time.Second
111- bashBudget = 30 * time.Second
112- waitDelay = 2 * time.Second
113-)
114 111
115 // outputCap is how much of a command's output is read back from disk, per end.112 // outputCap is how much of a command's output is read back from disk, per end.
116 // truncate() keeps a head and a tail, so those are what we read: a command113 // truncate() keeps a head and a tail, so those are what we read: a command
@@ -125,7 +122,7 @@ const outputCap = 1 << 20
125 //122 //
126 // Two things here are not obvious, and both are ticket 26.123 // Two things here are not obvious, and both are ticket 26.
127 //124 //
128-// The command runs under the TURN's context, with 30s of its own as a child of125+// The command runs under the TURN's context, with bashTimeout of its own as a child of
129 // it: cancelling the turn — Escape, Ctrl-C, /abort, or asking something else —126 // it: cancelling the turn — Escape, Ctrl-C, /abort, or asking something else —
130 // now kills the command. It used to hang off context.Background(), so a127 // now kills the command. It used to hang off context.Background(), so a
131 // cancellation never reached the command that was actually running.128 // cancellation never reached the command that was actually running.
@@ -144,7 +141,8 @@ const outputCap = 1 << 20
144 // descriptor and keeps working, which is the point — the user asked for that141 // descriptor and keeps working, which is the point — the user asked for that
145 // server — and writes into a file nobody reads.142 // server — and writes into a file nobody reads.
146 func runBash(ctx context.Context, dir, command string) (output string, failed bool) {143 func runBash(ctx context.Context, dir, command string) (output string, failed bool) {
147- ctx, cancel := context.WithTimeout(ctx, bashBudget)144+ budget := config.Cfg.BashTimeout
145+ ctx, cancel := context.WithTimeout(ctx, budget)
148 defer cancel()146 defer cancel()
149 147
150 sink, err := os.CreateTemp("", "mm-bash-*")148 sink, err := os.CreateTemp("", "mm-bash-*")
@@ -167,7 +165,7 @@ func runBash(ctx context.Context, dir, command string) (output string, failed bo
167 165
168 switch {166 switch {
169 case errors.Is(ctx.Err(), context.DeadlineExceeded):167 case errors.Is(ctx.Err(), context.DeadlineExceeded):
170- return output + "\n[error: 30s timeout exceeded]", true168+ return output + fmt.Sprintf("\n[error: %s timeout exceeded]", budget), true
171 case errors.Is(ctx.Err(), context.Canceled):169 case errors.Is(ctx.Err(), context.Canceled):
172 return output + "\n[error: cancelled]", true170 return output + "\n[error: cancelled]", true
173 case err != nil:171 case err != nil:
modified main.go +101 -9
@@ -33,8 +33,10 @@ import (
3333 "flag"
3434 "fmt"
3535 "os"
36+ "os/signal"
3637 "path/filepath"
3738 "strings"
39+ "time"
3840
3941 "mm/internal/acp"
4042 "mm/internal/agent"
@@ -72,19 +74,28 @@ func main() {
7274 // Handle --prompt/-p flag which can appear after the config file argument
7375 // We manually extract it before flag.Parse() so both orders work:
7476 // mm --prompt "hello" agent.yaml AND mm agent.yaml --prompt "hello"
75- promptArg := ""
77+ // The same goes for --ask and --ask-with-context, whose agent name follows
78+ // them: `mm agent.yaml --ask lucy -p "…"` must work like the flags-first
79+ // order, and flag.Parse stops at the first positional argument.
80+ promptArg, askArg, askContextArg := "", "", ""
7681 remainingArgs := []string{}
7782 for i := 1; i < len(os.Args); i++ {
7883 arg := os.Args[i]
79- if arg == "--prompt" || arg == "-p" {
80- if i+1 < len(os.Args) {
81- promptArg = os.Args[i+1]
82- i++ // skip the next arg (the prompt value)
83- } else {
84- promptArg = ""
85- }
86- } else {
84+ var target *string
85+ switch arg {
86+ case "--prompt", "-prompt", "-p":
87+ target = &promptArg
88+ case "--ask", "-ask":
89+ target = &askArg
90+ case "--ask-with-context", "-ask-with-context":
91+ target = &askContextArg
92+ default:
8793 remainingArgs = append(remainingArgs, arg)
94+ continue
95+ }
96+ if i+1 < len(os.Args) {
97+ *target = os.Args[i+1]
98+ i++ // skip the value
8899 }
89100 }
90101 // Replace os.Args for flag.Parse()
@@ -97,12 +108,20 @@ func main() {
97108 flag.StringVar(promptMode, "p", "", "shorthand for --prompt")
98109 showVersion := flag.Bool("version", false, "print the version and exit")
99110 showAbout := flag.Bool("about", false, "print what mm is, and exit")
111+ askAgent := flag.String("ask", "", "ask the agent `name` (a key of mmConfigs) the -p prompt, or stdin; print only its answer on stdout, and exit")
112+ askWithContext := flag.String("ask-with-context", "", "same as --ask, with the conversation read from stdin (question with -p, or after a \"---\" line)")
100113 flag.Parse()
101114
102115 // If prompt was extracted manually, use it (overrides flag value)
103116 if promptArg != "" {
104117 *promptMode = promptArg
105118 }
119+ if askArg != "" {
120+ *askAgent = askArg
121+ }
122+ if askContextArg != "" {
123+ *askWithContext = askContextArg
124+ }
106125
107126 // Track if --config was explicitly set via the flag
108127 configExplicit := false
@@ -137,6 +156,22 @@ func main() {
137156 os.Exit(1)
138157 }
139158
159+ // --ask: stdout carries the answer and nothing else, so everything human
160+ // goes to stderr, like in ACP mode. With no config named, the config of
161+ // the mm whose bash tool runs this command (MM_CONFIG, exported below) is
162+ // the one whose mmConfigs a skill means.
163+ askName, askContext := *askAgent, false
164+ if *askWithContext != "" {
165+ askName, askContext = *askWithContext, true
166+ }
167+ if askName != "" {
168+ ui.Out = os.Stderr
169+ spinner.Disable()
170+ if *configPath == "" {
171+ *configPath = os.Getenv(configEnv)
172+ }
173+ }
174+
140175 // In ACP mode, stdout belongs to JSON-RPC — the spec forbids anything else
141176 // on it. Everything below that prints (banner, warnings, errors) goes
142177 // through ui.Out, so ONE move sends it all to stderr, where the editor's
@@ -162,6 +197,11 @@ func main() {
162197 // named (env AGENT_CONFIG in the editor's settings). Either way the path
163198 // is made absolute here, so the banner can say exactly where it looked.
164199 config.Cfg.SkillsDir = resolveSkillsDir(config.Cfg.SkillsDir, path, *acpMode)
200+ exportSelf(path)
201+
202+ if askName != "" {
203+ os.Exit(runAsk(ctx, askName, askContext, *promptMode))
204+ }
165205
166206 // The other agents this one can ask (`mmConfigs`): their /ask-<name>
167207 // commands join the shared table before any front end reads it, and their
@@ -564,3 +604,55 @@ func resolveSkillsDir(dir, configPath string, acpMode bool) string {
564604 }
565605 return abs
566606 }
607+
608+// The two variables an mm exports to the commands its bash tool runs, so a
609+// skill can call this same mm back — `"$MM_BIN" --ask lucy -p …` — with this
610+// same configuration, without knowing where either lives.
611+const (
612+ binEnv = "MM_BIN"
613+ configEnv = "MM_CONFIG"
614+)
615+
616+// exportSelf sets MM_BIN and MM_CONFIG for the child processes of this mm.
617+func exportSelf(configPath string) {
618+ if self, err := os.Executable(); err == nil {
619+ _ = os.Setenv(binEnv, self)
620+ }
621+ if configPath == "" {
622+ return
623+ }
624+ if abs, err := filepath.Abs(configPath); err == nil {
625+ _ = os.Setenv(configEnv, abs)
626+ }
627+}
628+
629+// runAsk is `mm --ask <name>` / `mm --ask-with-context <name>`: one question to
630+// another agent, its answer on stdout, and the exit code — 0 answered, 1
631+// failed, 130 interrupted (Ctrl+C) — for the script or skill that called it.
632+func runAsk(ctx context.Context, name string, withContext bool, prompt string) int {
633+ ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
634+ defer stop()
635+ // A skill runs this through its agent's bash tool, which kills bash — not
636+ // us — when its budget runs out: stop with it rather than linger.
637+ ctx, cancel := context.WithCancel(ctx)
638+ defer cancel()
639+ ask.WatchParent(ctx, cancel, 500*time.Millisecond)
640+ req, err := ask.ReadCLIRequest(name, withContext, prompt, os.Stdin)
641+ if err != nil {
642+ say("[ask: %v]", err)
643+ return 1
644+ }
645+ pool := ask.NewPool()
646+ defer pool.Close()
647+ cwd, _ := os.Getwd()
648+ err = pool.CLI(ctx, req, cwd, os.Stdout, os.Stderr)
649+ switch {
650+ case err != nil && ctx.Err() != nil:
651+ say("[ask: interrupted]")
652+ return 130
653+ case err != nil:
654+ say("[ask: %v]", err)
655+ return 1
656+ }
657+ return 0
658+}
@@ -33,8 +33,10 @@ import (
33 "flag"33 "flag"
34 "fmt"34 "fmt"
35 "os"35 "os"
36+ "os/signal"
36 "path/filepath"37 "path/filepath"
37 "strings"38 "strings"
39+ "time"
38 40
39 "mm/internal/acp"41 "mm/internal/acp"
40 "mm/internal/agent"42 "mm/internal/agent"
@@ -72,19 +74,28 @@ func main() {
72 // Handle --prompt/-p flag which can appear after the config file argument74 // Handle --prompt/-p flag which can appear after the config file argument
73 // We manually extract it before flag.Parse() so both orders work:75 // We manually extract it before flag.Parse() so both orders work:
74 // mm --prompt "hello" agent.yaml AND mm agent.yaml --prompt "hello"76 // mm --prompt "hello" agent.yaml AND mm agent.yaml --prompt "hello"
75- promptArg := ""77+ // The same goes for --ask and --ask-with-context, whose agent name follows
78+ // them: `mm agent.yaml --ask lucy -p "…"` must work like the flags-first
79+ // order, and flag.Parse stops at the first positional argument.
80+ promptArg, askArg, askContextArg := "", "", ""
76 remainingArgs := []string{}81 remainingArgs := []string{}
77 for i := 1; i < len(os.Args); i++ {82 for i := 1; i < len(os.Args); i++ {
78 arg := os.Args[i]83 arg := os.Args[i]
79- if arg == "--prompt" || arg == "-p" {84+ var target *string
80- if i+1 < len(os.Args) {85+ switch arg {
81- promptArg = os.Args[i+1]86+ case "--prompt", "-prompt", "-p":
82- i++ // skip the next arg (the prompt value)87+ target = &promptArg
83- } else {88+ case "--ask", "-ask":
84- promptArg = ""89+ target = &askArg
85- }90+ case "--ask-with-context", "-ask-with-context":
86- } else {91+ target = &askContextArg
92+ default:
87 remainingArgs = append(remainingArgs, arg)93 remainingArgs = append(remainingArgs, arg)
94+ continue
95+ }
96+ if i+1 < len(os.Args) {
97+ *target = os.Args[i+1]
98+ i++ // skip the value
88 }99 }
89 }100 }
90 // Replace os.Args for flag.Parse()101 // Replace os.Args for flag.Parse()
@@ -97,12 +108,20 @@ func main() {
97 flag.StringVar(promptMode, "p", "", "shorthand for --prompt")108 flag.StringVar(promptMode, "p", "", "shorthand for --prompt")
98 showVersion := flag.Bool("version", false, "print the version and exit")109 showVersion := flag.Bool("version", false, "print the version and exit")
99 showAbout := flag.Bool("about", false, "print what mm is, and exit")110 showAbout := flag.Bool("about", false, "print what mm is, and exit")
111+ askAgent := flag.String("ask", "", "ask the agent `name` (a key of mmConfigs) the -p prompt, or stdin; print only its answer on stdout, and exit")
112+ askWithContext := flag.String("ask-with-context", "", "same as --ask, with the conversation read from stdin (question with -p, or after a \"---\" line)")
100 flag.Parse()113 flag.Parse()
101 114
102 // If prompt was extracted manually, use it (overrides flag value)115 // If prompt was extracted manually, use it (overrides flag value)
103 if promptArg != "" {116 if promptArg != "" {
104 *promptMode = promptArg117 *promptMode = promptArg
105 }118 }
119+ if askArg != "" {
120+ *askAgent = askArg
121+ }
122+ if askContextArg != "" {
123+ *askWithContext = askContextArg
124+ }
106 125
107 // Track if --config was explicitly set via the flag126 // Track if --config was explicitly set via the flag
108 configExplicit := false127 configExplicit := false
@@ -137,6 +156,22 @@ func main() {
137 os.Exit(1)156 os.Exit(1)
138 }157 }
139 158
159+ // --ask: stdout carries the answer and nothing else, so everything human
160+ // goes to stderr, like in ACP mode. With no config named, the config of
161+ // the mm whose bash tool runs this command (MM_CONFIG, exported below) is
162+ // the one whose mmConfigs a skill means.
163+ askName, askContext := *askAgent, false
164+ if *askWithContext != "" {
165+ askName, askContext = *askWithContext, true
166+ }
167+ if askName != "" {
168+ ui.Out = os.Stderr
169+ spinner.Disable()
170+ if *configPath == "" {
171+ *configPath = os.Getenv(configEnv)
172+ }
173+ }
174+
140 // In ACP mode, stdout belongs to JSON-RPC — the spec forbids anything else175 // In ACP mode, stdout belongs to JSON-RPC — the spec forbids anything else
141 // on it. Everything below that prints (banner, warnings, errors) goes176 // on it. Everything below that prints (banner, warnings, errors) goes
142 // through ui.Out, so ONE move sends it all to stderr, where the editor's177 // through ui.Out, so ONE move sends it all to stderr, where the editor's
@@ -162,6 +197,11 @@ func main() {
162 // named (env AGENT_CONFIG in the editor's settings). Either way the path197 // named (env AGENT_CONFIG in the editor's settings). Either way the path
163 // is made absolute here, so the banner can say exactly where it looked.198 // is made absolute here, so the banner can say exactly where it looked.
164 config.Cfg.SkillsDir = resolveSkillsDir(config.Cfg.SkillsDir, path, *acpMode)199 config.Cfg.SkillsDir = resolveSkillsDir(config.Cfg.SkillsDir, path, *acpMode)
200+ exportSelf(path)
201+
202+ if askName != "" {
203+ os.Exit(runAsk(ctx, askName, askContext, *promptMode))
204+ }
165 205
166 // The other agents this one can ask (`mmConfigs`): their /ask-<name>206 // The other agents this one can ask (`mmConfigs`): their /ask-<name>
167 // commands join the shared table before any front end reads it, and their207 // commands join the shared table before any front end reads it, and their
@@ -564,3 +604,55 @@ func resolveSkillsDir(dir, configPath string, acpMode bool) string {
564 }604 }
565 return abs605 return abs
566 }606 }
607+
608+// The two variables an mm exports to the commands its bash tool runs, so a
609+// skill can call this same mm back — `"$MM_BIN" --ask lucy -p …` — with this
610+// same configuration, without knowing where either lives.
611+const (
612+ binEnv = "MM_BIN"
613+ configEnv = "MM_CONFIG"
614+)
615+
616+// exportSelf sets MM_BIN and MM_CONFIG for the child processes of this mm.
617+func exportSelf(configPath string) {
618+ if self, err := os.Executable(); err == nil {
619+ _ = os.Setenv(binEnv, self)
620+ }
621+ if configPath == "" {
622+ return
623+ }
624+ if abs, err := filepath.Abs(configPath); err == nil {
625+ _ = os.Setenv(configEnv, abs)
626+ }
627+}
628+
629+// runAsk is `mm --ask <name>` / `mm --ask-with-context <name>`: one question to
630+// another agent, its answer on stdout, and the exit code — 0 answered, 1
631+// failed, 130 interrupted (Ctrl+C) — for the script or skill that called it.
632+func runAsk(ctx context.Context, name string, withContext bool, prompt string) int {
633+ ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
634+ defer stop()
635+ // A skill runs this through its agent's bash tool, which kills bash — not
636+ // us — when its budget runs out: stop with it rather than linger.
637+ ctx, cancel := context.WithCancel(ctx)
638+ defer cancel()
639+ ask.WatchParent(ctx, cancel, 500*time.Millisecond)
640+ req, err := ask.ReadCLIRequest(name, withContext, prompt, os.Stdin)
641+ if err != nil {
642+ say("[ask: %v]", err)
643+ return 1
644+ }
645+ pool := ask.NewPool()
646+ defer pool.Close()
647+ cwd, _ := os.Getwd()
648+ err = pool.CLI(ctx, req, cwd, os.Stdout, os.Stderr)
649+ switch {
650+ case err != nil && ctx.Err() != nil:
651+ say("[ask: interrupted]")
652+ return 130
653+ case err != nil:
654+ say("[ask: %v]", err)
655+ return 1
656+ }
657+ return 0
658+}