🛟 Updated.
382fecb parent: 8654e3f modified
.tickets/issues/0052-buitin-acp-tool-to-query-other-agent-like-mini-m.yaml +2 -1 | @@ -6,7 +6,7 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-29T05:18:13.897Z |
| 9 | -updatedAt: 2026-09-29T06:25:58.324Z | |
| 9 | +updatedAt: 2026-10-02T21:15:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - priority::high |
| 12 | 12 | - kind::feature |
| @@ -15,6 +15,7 @@ labels: | ||
| 15 | 15 | milestone: v0.3.0 |
| 16 | 16 | related: |
| 17 | 17 | - 33 |
| 18 | + - 66 | |
| 18 | 19 | body: "" |
| 19 | 20 | tasks: [] |
| 20 | 21 | comments: [] |
| @@ -6,7 +6,7 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-29T05:18:13.897Z | 8 | createdAt: 2026-09-29T05:18:13.897Z |
| 9 | -updatedAt: 2026-09-29T06:25:58.324Z | 9 | +updatedAt: 2026-10-02T21:15:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - priority::high | 11 | - priority::high |
| 12 | - kind::feature | 12 | - kind::feature |
| @@ -15,6 +15,7 @@ labels: | |||
| 15 | milestone: v0.3.0 | 15 | milestone: v0.3.0 |
| 16 | related: | 16 | related: |
| 17 | - 33 | 17 | - 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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-02T18:48:36.275Z |
| 9 | -updatedAt: 2026-10-02T20:30:00.000Z | |
| 9 | +updatedAt: 2026-10-02T21:15:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | related: |
| 13 | + - 52 | |
| 13 | 14 | - 57 |
| 14 | 15 | - 59 |
| 15 | 16 | body: | |
| @@ -6,10 +6,11 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-02T18:48:36.275Z | 8 | createdAt: 2026-10-02T18:48:36.275Z |
| 9 | -updatedAt: 2026-10-02T20:30:00.000Z | 9 | +updatedAt: 2026-10-02T21:15:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | related: | 12 | related: |
| 13 | + - 52 | ||
| 13 | - 57 | 14 | - 57 |
| 14 | - 59 | 15 | - 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | -id: 66 | |
| 2 | +id: 71 | |
| 3 | 3 | title: Mini Me as a SDK |
| 4 | 4 | state: open |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-02T05:32:33.878Z |
| 9 | -updatedAt: 2026-10-02T05:32:33.878Z | |
| 9 | +updatedAt: 2026-10-02T21:15:00.000Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | body: "" |
| 12 | 12 | 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: 66 | 2 | +id: 71 |
| 3 | title: Mini Me as a SDK | 3 | title: Mini Me as a SDK |
| 4 | state: open | 4 | state: open |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-02T05:32:33.878Z | 8 | createdAt: 2026-10-02T05:32:33.878Z |
| 9 | -updatedAt: 2026-10-02T05:32:33.878Z | 9 | +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 | ||
| 25 | 25 | mmConfigs: |
| 26 | 26 | jan: agent.jan-nano.yaml |
| 27 | 27 | 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 | 33 | # Only when the daemon was started with LLMMAN_API_KEYS (mandatory when it |
| 30 | 34 | # listens beyond loopback); unset otherwise. The key never goes in this file. |
| @@ -78,7 +82,9 @@ maxTurns: 40 | ||
| 78 | 82 | |
| 79 | 83 | # Where the `read_skill` tool looks for its markdown procedures. |
| 80 | 84 | # 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 | |
| 82 | 88 | |
| 83 | 89 | # Persistent sessions, per project. |
| 84 | 90 | # When enabled, every conversation is saved as one JSON file under `dir`. |
| @@ -177,7 +183,13 @@ sampling: | ||
| 177 | 183 | max_tokens: 4096 |
| 178 | 184 | |
| 179 | 185 | # 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 | |
| 181 | 193 | |
| 182 | 194 | # Silence allowed while the model reads the prompt, before the first token: |
| 183 | 195 | # 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.yaml | 26 | jan: agent.jan-nano.yaml |
| 27 | lucy: agent.lucy.yaml | 27 | 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 it | 33 | # 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: ../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 | ||
| 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: 4096 | 183 | 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: 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 | ||
| 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 its | 195 | # 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) | ||
| 14 | 14 | - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demo-llmman/` |
| 15 | 15 | - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demo-ollama/` |
| 16 | 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 | 18 | - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md) |
| 18 | 19 | - [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 | 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/` |
| @@ -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 editor | 19 | - [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 @@ | ||
| 22 | 22 | |
| 23 | 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 | 29 | ## Rejected alternatives |
| 26 | 30 | |
| 27 | 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. |
| @@ -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 alternatives | 29 | ## 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 @@ | ||
| 6 | 6 | |
| 7 | 7 | ``` |
| 8 | 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 | 13 | | Element | Type | Default | Description | |
| @@ -17,6 +19,8 @@ mm [-config <path>] [-acp] [-tui] [-prompt <text>|-p <text>] [--version] [--abou | ||
| 17 | 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 | 20 | | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. | |
| 19 | 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 | 24 | | `-h` | — | — | Print the flag usage and exit. | |
| 21 | 25 | |
| 22 | 26 | ## Version |
| @@ -56,6 +60,8 @@ The first existing source wins. | ||
| 56 | 60 | | `AGENT_BASE_URL` | Overrides `baseUrl` and disables the fallback probe. | |
| 57 | 61 | | `DMR_BASE_URL` | Same as `AGENT_BASE_URL`, for the `dmr` provider only; `AGENT_BASE_URL` wins when both are set. | |
| 58 | 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 | 65 | | `MM_ASK_DEPTH` | How many `/ask` levels deep this agent runs (unset = 0). Set by `/ask`; at 2, `/ask` is refused. | |
| 60 | 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 | 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 | 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 | 139 | ## Exit codes |
| 121 | 140 | |
| 122 | 141 | | Code | Cause | |
| 123 | 142 | |------|-------| |
| 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). | | |
| 126 | 146 | |
| 127 | 147 | ## Build and install |
| 128 | 148 | |
| @@ -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 | ## Version | 26 | ## 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 codes | 139 | ## 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 install | 147 | ## Build and install |
| 128 | 148 | ||
modified
docs/en/reference/configuration.md +2 -0 | @@ -25,6 +25,7 @@ | ||
| 25 | 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 | 26 | | `watchdogTimeout` | duration | `20s` | Silence tolerated between two streamed tokens before the connection is declared hung. Doubled for summary requests. | |
| 27 | 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 | 29 | | `context` | map | see below | Automatic history compression. | |
| 29 | 30 | | `sessions` | map | see below | Persistent, per-project sessions. | |
| 30 | 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 | 119 | | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | an extension without its dot | |
| 119 | 120 | | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | two servers on one extension | |
| 120 | 121 | | `permissions must be "auto" or "ask"` | any other value | |
| 122 | +| `bashTimeout must be > 0` | zero or negative | | |
| 121 | 123 | | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` | |
| 122 | 124 | | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` | |
| 123 | 125 | | `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: | ||
| 14 | 14 | - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demo-llmman/` |
| 15 | 15 | - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demo-ollama/` |
| 16 | 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 | 18 | - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md) |
| 18 | 19 | - [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 | 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/` |
| @@ -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 éditeur | 19 | - [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 @@ | ||
| 22 | 22 | |
| 23 | 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 | 29 | ## Alternatives écartées |
| 26 | 30 | |
| 27 | 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. |
| @@ -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ées | 29 | ## 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 @@ | ||
| 6 | 6 | |
| 7 | 7 | ``` |
| 8 | 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 | 13 | | Élément | Type | Défaut | Description | |
| @@ -17,6 +19,8 @@ mm [-config <chemin>] [-acp] [-tui] [-prompt <texte>|-p <texte>] [--version] [-- | ||
| 17 | 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 | 20 | | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. | |
| 19 | 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 | 24 | | `-h` | — | — | Affiche l'usage des options et quitte. | |
| 21 | 25 | |
| 22 | 26 | ## Version |
| @@ -56,6 +60,8 @@ La première source existante gagne. | ||
| 56 | 60 | | `AGENT_BASE_URL` | Surcharge `baseUrl` et désactive la sonde de repli. | |
| 57 | 61 | | `DMR_BASE_URL` | Comme `AGENT_BASE_URL`, pour le fournisseur `dmr` seulement ; `AGENT_BASE_URL` gagne quand les deux sont définies. | |
| 58 | 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 | 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 | 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 | 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 | 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 | 139 | ## Codes de sortie |
| 121 | 140 | |
| 122 | 141 | | Code | Cause | |
| 123 | 142 | |------|-------| |
| 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). | | |
| 126 | 146 | |
| 127 | 147 | ## Compilation et installation |
| 128 | 148 | |
| @@ -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 | ## Version | 26 | ## 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 sortie | 139 | ## 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 installation | 147 | ## Compilation et installation |
| 128 | 148 | ||
modified
docs/fr/reference/configuration.md +2 -0 | @@ -25,6 +25,7 @@ | ||
| 25 | 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 | 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 | 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 | 29 | | `context` | dictionnaire | voir ci-dessous | Compression automatique de l'historique. | |
| 29 | 30 | | `sessions` | dictionnaire | voir ci-dessous | Sessions persistantes, par projet. | |
| 30 | 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 | 119 | | `lsp.servers[N].extensions: "go" must start with a dot, like ".go"` | une extension sans son point | |
| 119 | 120 | | `lsp.servers[N]: extension ".ts" is already served by "typescript"` | deux serveurs sur une même extension | |
| 120 | 121 | | `permissions must be "auto" or "ask"` | toute autre valeur | |
| 122 | +| `bashTimeout must be > 0` | zéro ou négative | | |
| 121 | 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 | 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 | 125 | | `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) { | ||
| 94 | 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 | +} | |
| @@ -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 { | ||
| 119 | 119 | // Never shorter than WatchdogTimeout. |
| 120 | 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 | 129 | // Context: when and how the conversation history is compressed |
| 123 | 130 | // (internal/compact). Off unless `context.enabled: true`. The window it |
| 124 | 131 | // measures against is the top-level ContextWindow above — or, when that is |
| @@ -456,6 +463,7 @@ which ones you left running.`, | ||
| 456 | 463 | }, |
| 457 | 464 | WatchdogTimeout: 20 * time.Second, |
| 458 | 465 | FirstTokenTimeout: 3 * time.Minute, |
| 466 | + BashTimeout: 30 * time.Second, | |
| 459 | 467 | Sessions: SessionsConfig{ |
| 460 | 468 | Enabled: false, |
| 461 | 469 | Dir: ".mm/sessions", |
| @@ -605,6 +613,8 @@ func (c Config) validate() error { | ||
| 605 | 613 | return errors.New("contextWindow must be >= 0") |
| 606 | 614 | case c.FirstTokenTimeout < 0: |
| 607 | 615 | return errors.New("firstTokenTimeout must be >= 0") |
| 616 | + case c.BashTimeout <= 0: | |
| 617 | + return errors.New("bashTimeout must be > 0") | |
| 608 | 618 | case c.MaxOutput <= 0: |
| 609 | 619 | return errors.New("maxOutput must be > 0") |
| 610 | 620 | 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 compressed | 129 | // Context: when and how the conversation history is compressed |
| 123 | // (internal/compact). Off unless `context.enabled: true`. The window it | 130 | // (internal/compact). Off unless `context.enabled: true`. The window it |
| 124 | // measures against is the top-level ContextWindow above — or, when that is | 131 | // 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) { | ||
| 88 | 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 | +} | |
| @@ -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) { | ||
| 105 | 105 | printMu.Unlock() |
| 106 | 106 | } |
| 107 | 107 | |
| 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 | |
| 114 | 111 | |
| 115 | 112 | // outputCap is how much of a command's output is read back from disk, per end. |
| 116 | 113 | // 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 | 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 of | |
| 125 | +// The command runs under the TURN's context, with bashTimeout of its own as a child of | |
| 129 | 126 | // it: cancelling the turn — Escape, Ctrl-C, /abort, or asking something else — |
| 130 | 127 | // now kills the command. It used to hang off context.Background(), so a |
| 131 | 128 | // cancellation never reached the command that was actually running. |
| @@ -144,7 +141,8 @@ const outputCap = 1 << 20 | ||
| 144 | 141 | // descriptor and keeps working, which is the point — the user asked for that |
| 145 | 142 | // server — and writes into a file nobody reads. |
| 146 | 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 | 146 | defer cancel() |
| 149 | 147 | |
| 150 | 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 | 166 | switch { |
| 169 | 167 | 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 | |
| 171 | 169 | case errors.Is(ctx.Err(), context.Canceled): |
| 172 | 170 | return output + "\n[error: cancelled]", true |
| 173 | 171 | 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 a | 108 | +// 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 command | 113 | // 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 of | 125 | +// 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 a | 127 | // 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 that | 141 | // 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]", true | 168 | + 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]", true | 170 | return output + "\n[error: cancelled]", true |
| 173 | case err != nil: | 171 | case err != nil: |
modified
main.go +101 -9 | @@ -33,8 +33,10 @@ import ( | ||
| 33 | 33 | "flag" |
| 34 | 34 | "fmt" |
| 35 | 35 | "os" |
| 36 | + "os/signal" | |
| 36 | 37 | "path/filepath" |
| 37 | 38 | "strings" |
| 39 | + "time" | |
| 38 | 40 | |
| 39 | 41 | "mm/internal/acp" |
| 40 | 42 | "mm/internal/agent" |
| @@ -72,19 +74,28 @@ func main() { | ||
| 72 | 74 | // Handle --prompt/-p flag which can appear after the config file argument |
| 73 | 75 | // We manually extract it before flag.Parse() so both orders work: |
| 74 | 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 | 81 | remainingArgs := []string{} |
| 77 | 82 | for i := 1; i < len(os.Args); i++ { |
| 78 | 83 | 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: | |
| 87 | 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 | 101 | // Replace os.Args for flag.Parse() |
| @@ -97,12 +108,20 @@ func main() { | ||
| 97 | 108 | flag.StringVar(promptMode, "p", "", "shorthand for --prompt") |
| 98 | 109 | showVersion := flag.Bool("version", false, "print the version and exit") |
| 99 | 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 | 113 | flag.Parse() |
| 101 | 114 | |
| 102 | 115 | // If prompt was extracted manually, use it (overrides flag value) |
| 103 | 116 | if promptArg != "" { |
| 104 | 117 | *promptMode = promptArg |
| 105 | 118 | } |
| 119 | + if askArg != "" { | |
| 120 | + *askAgent = askArg | |
| 121 | + } | |
| 122 | + if askContextArg != "" { | |
| 123 | + *askWithContext = askContextArg | |
| 124 | + } | |
| 106 | 125 | |
| 107 | 126 | // Track if --config was explicitly set via the flag |
| 108 | 127 | configExplicit := false |
| @@ -137,6 +156,22 @@ func main() { | ||
| 137 | 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 | 175 | // In ACP mode, stdout belongs to JSON-RPC — the spec forbids anything else |
| 141 | 176 | // on it. Everything below that prints (banner, warnings, errors) goes |
| 142 | 177 | // through ui.Out, so ONE move sends it all to stderr, where the editor's |
| @@ -162,6 +197,11 @@ func main() { | ||
| 162 | 197 | // named (env AGENT_CONFIG in the editor's settings). Either way the path |
| 163 | 198 | // is made absolute here, so the banner can say exactly where it looked. |
| 164 | 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 | 206 | // The other agents this one can ask (`mmConfigs`): their /ask-<name> |
| 167 | 207 | // 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 | 605 | 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 | +} | |
| @@ -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 argument | 74 | // 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 = promptArg | 117 | *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 flag | 126 | // Track if --config was explicitly set via the flag |
| 108 | configExplicit := false | 127 | 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 else | 175 | // In ACP mode, stdout belongs to JSON-RPC — the spec forbids anything else |
| 141 | // on it. Everything below that prints (banner, warnings, errors) goes | 176 | // 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's | 177 | // 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 path | 197 | // 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 their | 207 | // 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 abs | 605 | 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 | +} | ||