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

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

Download ZIP

Merge feature/mini-mes into main

rickub committed 2026-10-07T09:09:51Z Browse files
ad14030 parents: 8941766 2d0776d
added .tickets/issues/0079-auto-update-of-the-system-instructions-and-skill.yaml +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 79
3+title: Auto update of the system instructions and skills
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-07T09:00:46.595Z
9+updatedAt: 2026-10-07T09:00:46.595Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 79
3+title: Auto update of the system instructions and skills
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-10-07T09:00:46.595Z
9+updatedAt: 2026-10-07T09:00:46.595Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
modified .vscode/settings.json +1 -1
@@ -2,7 +2,7 @@
22 "workbench.iconTheme": "material-icon-theme",
33 "workbench.colorTheme": "GitHub Light Colorblind (Beta)",
44 "editor.fontSize": 14,
5- "terminal.integrated.fontSize": 14,
5+ "terminal.integrated.fontSize": 16,
66 "editor.insertSpaces": true,
77 "editor.tabSize": 4,
88 "editor.detectIndentation": true,
@@ -2,7 +2,7 @@
2 "workbench.iconTheme": "material-icon-theme",2 "workbench.iconTheme": "material-icon-theme",
3 "workbench.colorTheme": "GitHub Light Colorblind (Beta)",3 "workbench.colorTheme": "GitHub Light Colorblind (Beta)",
4 "editor.fontSize": 14,4 "editor.fontSize": 14,
5- "terminal.integrated.fontSize": 14,5+ "terminal.integrated.fontSize": 16,
6 "editor.insertSpaces": true,6 "editor.insertSpaces": true,
7 "editor.tabSize": 4,7 "editor.tabSize": 4,
8 "editor.detectIndentation": true,8 "editor.detectIndentation": true,
modified demo-llmman/README.en.md +35 -33
@@ -6,22 +6,26 @@ This folder shows how one mini-me agent (Bob) can put questions to other mini-me
66
77 | File | Role |
88 |---|---|
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/`. |
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`, with a description and, for Lucy, her own `yolo`. |
1010 | `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), a full agent with its tools. |
1111 | `agent.lucy.yaml` | **Lucy** (`hf.co/Menlo/Lucy-gguf:Q4_K_M`), a full agent with its tools. |
1212 | `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. |
1613 | `skills/bg-jobs` | Copy of the general skill (Bob's prompt refers to it). |
1714
1815 In `agent.llmman.yaml`:
1916
2017 ```yaml
2118 mmConfigs:
22- jan: agent.jan-nano.yaml
23- lucy: agent.lucy.yaml
24- reviewer: agent.lucy-reviewer.yaml
19+ jan:
20+ path: agent.jan-nano.yaml
21+ description: Research and fact questions — looks things up and answers in a few lines.
22+ lucy:
23+ path: agent.lucy.yaml
24+ description: A coding agent — reads, writes and runs code in this project. …
25+ yolo: true
26+ reviewer:
27+ path: agent.lucy-reviewer.yaml
28+ description: A second opinion on an answer or a claim from this conversation — call it with with_context true. No tools.
2529 ```
2630
2731 ## Prerequisites
@@ -86,58 +90,56 @@ printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, P
8690 - 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.
8791 - **stdout holds the answer only** (`answer=$(../mm agent.llmman.yaml --ask jan -p "…")` works); the label, the tool calls and errors go to stderr.
8892 - 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`.
93+- The asked agent's tool calls follow its `mmConfigs` entry's `yolo` (Lucy: `true`, they run), else `agent.llmman.yaml`'s (`false`: rejected, nobody can answer). For an agent that should only answer, use `reviewer`.
9094
91-## 3. Let Bob ask the others through his skills
95+## 3. Let Bob decide: the `ask_agent` tool
9296
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:
97+Since `mmConfigs` is not empty, Bob has the built-in `ask_agent` tool. Its description lists Jan, Lucy and the reviewer with their `description`: Bob chooses, no skill involved. Ask in plain words:
9498
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 |
99+| You say | Bob calls |
100+|---|---|
101+| "Ask Jan what the capital of Peru is." | `ask_agent` → `jan` |
102+| "I think Go's goroutines are OS threads. Get a second opinion on that claim." | `ask_agent` → `reviewer`, with `with_context` |
103+| "Ask Lucy to add a test for main.go and run it." | `ask_agent` → `lucy`, whose writes and commands run without asking (`yolo: true` in her entry) |
100104
101-A real run (`second-opinion`, 97 s in all):
105+Bob has `yolo: false`: you approve each hand-off first, with the prompt he wrote. In the window (`../mm -tui agent.llmman.yaml`), the asked agent is named while it works:
102106
103107 ```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.
108+mm [Jan (coding agent)] · Jan-nano:Q4_K_M @ llmman · asked by mm [Bob (coordinator)]
109+…
110+‣ mm [Jan (coding agent)]
111+The capital of Peru is Lima.
114112 ```
115113
116-`compare-agents` has not been tried live yet.
114+then the header comes back to Bob, who concludes under `‣ mm [Bob (coordinator)]`. Measured: 6 s for Jan, 5.6 s for the reviewer (`mm -p`, models already loaded).
115+
116+The answer comes back to Bob as the tool's result, in the same turn; it is not added to the history as with `/ask`.
117117
118118 ## Time settings
119119
120120 A small local model can take a minute to answer, so `agent.llmman.yaml` raises two limits:
121121
122122 ```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)
123+watchdogTimeout: 3m # silence tolerated; during ask_agent each event of the asked agent counts as a sign of life
124+bashTimeout: 3m # how long one bash command may run (default 30s), for a script running mm --ask
125125 ```
126126
127-With the defaults, a skill that asks an agent ends in `[error: 30s timeout exceeded]` or `connection hang detected after 30s`.
127+With `ask_agent`, only a long silence of the asked agent (its model loading, a long first prompt being read) can still exceed `watchdogTimeout`.
128128
129129 ## Good to know
130130
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.
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 opinions.
132+- **Once**: Bob's model (4B) sometimes re-ran the same question; his prompt tells him to call `ask_agent` once per task.
133+- **English**: these models answer best in English.
134134 - **No sessions for the asked agents**: they run with `AGENT_SESSIONS=off`, so your `/sessions` lists Bob's only.
135135 - **Limited depth**: Bob may ask Jan, Jan may ask another agent, but that one cannot ask anybody (`MM_ASK_DEPTH`).
136136 - **Names**: a skill must not be named like a generated command (`ask-jan`…); mm warns at start-up.
137+- **The delegation skills** (`delegate-to-jan`, `second-opinion`, `compare-agents`) that had Bob run `mm --ask` were removed in favour of `ask_agent`; they remain in the git history (commit 8941766), and the skills guide shows one.
137138
138139 ## Going further
139140
140141 - [How to ask another mm agent from your session](../docs/en/how-to/ask-another-agent.md)
142+- [How to let your agent call other agents](../docs/en/how-to/let-the-agent-call-other-agents.md)
141143 - [How to ask another agent from a skill or a script](../docs/en/how-to/ask-another-agent-from-a-skill.md)
142144 - [Slash commands reference](../docs/en/reference/slash-commands.md#asking-another-agent) · [Command-line reference](../docs/en/reference/cli.md#ask-mode-specifics)
143145 - [Why it is built this way](../docs/en/explanation/asking-another-agent.md)
@@ -6,22 +6,26 @@ This folder shows how one mini-me agent (Bob) can put questions to other mini-me
6 6
7 | File | Role |7 | File | Role |
8 |---|---|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/`. |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`, with a description and, for Lucy, her own `yolo`. |
10 | `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), a full agent with its tools. |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. |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. |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). |13 | `skills/bg-jobs` | Copy of the general skill (Bob's prompt refers to it). |
17 14
18 In `agent.llmman.yaml`:15 In `agent.llmman.yaml`:
19 16
20 ```yaml17 ```yaml
21 mmConfigs:18 mmConfigs:
22- jan: agent.jan-nano.yaml19+ jan:
23- lucy: agent.lucy.yaml20+ path: agent.jan-nano.yaml
24- reviewer: agent.lucy-reviewer.yaml21+ description: Research and fact questions — looks things up and answers in a few lines.
22+ lucy:
23+ path: agent.lucy.yaml
24+ description: A coding agent — reads, writes and runs code in this project. …
25+ yolo: true
26+ reviewer:
27+ path: agent.lucy-reviewer.yaml
28+ description: A second opinion on an answer or a claim from this conversation — call it with with_context true. No tools.
25 ```29 ```
26 30
27 ## Prerequisites31 ## Prerequisites
@@ -86,58 +90,56 @@ printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, P
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.90 - 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.91 - **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.92 - 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`.93+- The asked agent's tool calls follow its `mmConfigs` entry's `yolo` (Lucy: `true`, they run), else `agent.llmman.yaml`'s (`false`: rejected, nobody can answer). For an agent that should only answer, use `reviewer`.
90 94
91-## 3. Let Bob ask the others through his skills95+## 3. Let Bob decide: the `ask_agent` tool
92 96
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:97+Since `mmConfigs` is not empty, Bob has the built-in `ask_agent` tool. Its description lists Jan, Lucy and the reviewer with their `description`: Bob chooses, no skill involved. Ask in plain words:
94 98
95-| You say | Skill | Command Bob runs |99+| You say | Bob calls |
96-|---|---|---|100+|---|---|
97-| "Ask Jan what the capital of Peru is." | `delegate-to-jan` | `"$MM_BIN" --ask jan -p "What is the capital of Peru?"` |101+| "Ask Jan what the capital of Peru is." | `ask_agent` → `jan` |
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' …` |102+| "I think Go's goroutines are OS threads. Get a second opinion on that claim." | `ask_agent` → `reviewer`, with `with_context` |
99-| "Ask both Jan and Lucy: what is a closure in Go?" | `compare-agents` | both `--ask` in one command |103+| "Ask Lucy to add a test for main.go and run it." | `ask_agent` → `lucy`, whose writes and commands run without asking (`yolo: true` in her entry) |
100 104
101-A real run (`second-opinion`, 97 s in all):105+Bob has `yolo: false`: you approve each hand-off first, with the prompt he wrote. In the window (`../mm -tui agent.llmman.yaml`), the asked agent is named while it works:
102 106
103 ```text107 ```text
104-📖 read_skill: second-opinion108+mm [Jan (coding agent)] · Jan-nano:Q4_K_M @ llmman · asked by mm [Bob (coordinator)]
105-🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'109+…
106-User: I think Go's goroutines are OS threads.110+‣ mm [Jan (coding agent)]
107-Assistant: Actually, Go goroutines are not OS threads. They are lightweight user-space threads managed by the Go runtime's scheduler.111+The capital of Peru is Lima.
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 ```112 ```
115 113
116-`compare-agents` has not been tried live yet.114+then the header comes back to Bob, who concludes under `‣ mm [Bob (coordinator)]`. Measured: 6 s for Jan, 5.6 s for the reviewer (`mm -p`, models already loaded).
115+
116+The answer comes back to Bob as the tool's result, in the same turn; it is not added to the history as with `/ask`.
117 117
118 ## Time settings118 ## Time settings
119 119
120 A small local model can take a minute to answer, so `agent.llmman.yaml` raises two limits:120 A small local model can take a minute to answer, so `agent.llmman.yaml` raises two limits:
121 121
122 ```yaml122 ```yaml
123-watchdogTimeout: 3m # the watchdog keeps counting while a command runs (ticket 57)123+watchdogTimeout: 3m # silence tolerated; during ask_agent each event of the asked agent counts as a sign of life
124-bashTimeout: 3m # how long one bash command may run (default 30s)124+bashTimeout: 3m # how long one bash command may run (default 30s), for a script running mm --ask
125 ```125 ```
126 126
127-With the defaults, a skill that asks an agent ends in `[error: 30s timeout exceeded]` or `connection hang detected after 30s`.127+With `ask_agent`, only a long silence of the asked agent (its model loading, a long first prompt being read) can still exceed `watchdogTimeout`.
128 128
129 ## Good to know129 ## Good to know
130 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`.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 opinions.
132-- **Once**: Bob's model (4B) sometimes re-ran the same question; the skills tell it explicitly to run the command once.132+- **Once**: Bob's model (4B) sometimes re-ran the same question; his prompt tells him to call `ask_agent` once per task.
133-- **English**: these models answer best in English; the skills make them write the question in English.133+- **English**: these models answer best in English.
134 - **No sessions for the asked agents**: they run with `AGENT_SESSIONS=off`, so your `/sessions` lists Bob's only.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`).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.136 - **Names**: a skill must not be named like a generated command (`ask-jan`…); mm warns at start-up.
137+- **The delegation skills** (`delegate-to-jan`, `second-opinion`, `compare-agents`) that had Bob run `mm --ask` were removed in favour of `ask_agent`; they remain in the git history (commit 8941766), and the skills guide shows one.
137 138
138 ## Going further139 ## Going further
139 140
140 - [How to ask another mm agent from your session](../docs/en/how-to/ask-another-agent.md)141 - [How to ask another mm agent from your session](../docs/en/how-to/ask-another-agent.md)
142+- [How to let your agent call other agents](../docs/en/how-to/let-the-agent-call-other-agents.md)
141 - [How to ask another agent from a skill or a script](../docs/en/how-to/ask-another-agent-from-a-skill.md)143 - [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)144 - [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)145 - [Why it is built this way](../docs/en/explanation/asking-another-agent.md)
modified demo-llmman/README.md +35 -33
@@ -6,22 +6,26 @@ Ce dossier montre comment un agent mini-me (Bob) peut poser des questions à d'a
66
77 | Fichier | Rôle |
88 |---|---|
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/`. |
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`, avec une description et, pour Lucy, son propre `yolo`. |
1010 | `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), agent complet avec ses outils. |
1111 | `agent.lucy.yaml` | **Lucy** (`hf.co/Menlo/Lucy-gguf:Q4_K_M`), agent complet avec ses outils. |
1212 | `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. |
1613 | `skills/bg-jobs` | Copie de la skill générale (le prompt de Bob y fait référence). |
1714
1815 Dans `agent.llmman.yaml` :
1916
2017 ```yaml
2118 mmConfigs:
22- jan: agent.jan-nano.yaml
23- lucy: agent.lucy.yaml
24- reviewer: agent.lucy-reviewer.yaml
19+ jan:
20+ path: agent.jan-nano.yaml
21+ description: Research and fact questions — looks things up and answers in a few lines.
22+ lucy:
23+ path: agent.lucy.yaml
24+ description: A coding agent — reads, writes and runs code in this project. …
25+ yolo: true
26+ reviewer:
27+ path: agent.lucy-reviewer.yaml
28+ description: A second opinion on an answer or a claim from this conversation — call it with with_context true. No tools.
2529 ```
2630
2731 ## Prérequis
@@ -86,58 +90,56 @@ printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, P
8690 - 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.
8791 - **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.
8892 - 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`.
93+- Les appels d'outils de l'agent appelé suivent le `yolo` de son entrée dans `mmConfigs` (Lucy : `true`, ils s'exécutent), sinon celui de `agent.llmman.yaml` (`false` : refusés, personne ne peut répondre). Pour un agent qui doit seulement répondre, utilisez `reviewer`.
9094
91-## 3. Laisser Bob interroger les autres via ses skills
95+## 3. Laisser Bob décider : l'outil `ask_agent`
9296
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 :
97+Comme `mmConfigs` n'est pas vide, Bob dispose de l'outil intégré `ask_agent`. Sa description liste Jan, Lucy et le relecteur avec leur `description` : c'est Bob qui choisit, sans skill. Demandez en langage courant :
9498
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 |
99+| Vous dites | Bob appelle |
100+|---|---|
101+| « Ask Jan what the capital of Peru is. » | `ask_agent` → `jan` |
102+| « I think Go's goroutines are OS threads. Get a second opinion on that claim. » | `ask_agent` → `reviewer`, avec `with_context` |
103+| « Ask Lucy to add a test for main.go and run it. » | `ask_agent` → `lucy`, dont les écritures et commandes s'exécutent sans demander (`yolo: true` dans son entrée) |
100104
101-Exemple réel (`second-opinion`, 97 s au total) :
105+Bob a `yolo: false` : vous approuvez d'abord chaque passage de main, avec le prompt qu'il a écrit. Dans la fenêtre (`../mm -tui agent.llmman.yaml`), l'agent appelé est nommé pendant qu'il travaille :
102106
103107 ```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.
108+mm [Jan (coding agent)] · Jan-nano:Q4_K_M @ llmman · asked by mm [Bob (coordinator)]
109+…
110+‣ mm [Jan (coding agent)]
111+The capital of Peru is Lima.
114112 ```
115113
116-`compare-agents` n'a pas encore été testée en conditions réelles.
114+puis l'en-tête revient à Bob, qui conclut sous `‣ mm [Bob (coordinator)]`. Mesuré : 6 s pour Jan, 5,6 s pour le relecteur (`mm -p`, modèles déjà chargés).
115+
116+La réponse revient à Bob comme résultat de l'outil, dans le même tour ; elle n'est pas ajoutée à l'historique comme avec `/ask`.
117117
118118 ## Réglages de temps
119119
120120 Un petit modèle local peut mettre une minute à répondre. `agent.llmman.yaml` relève donc deux délais :
121121
122122 ```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)
123+watchdogTimeout: 3m # silence toléré ; pendant ask_agent, chaque événement de l'agent appelé compte comme signe de vie
124+bashTimeout: 3m # durée maximale d'une commande bash (30s par défaut), pour un script qui lance mm --ask
125125 ```
126126
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`.
127+Avec `ask_agent`, seul un long silence de l'agent appelé (le chargement de son modèle, la lecture d'un long premier prompt) peut encore dépasser `watchdogTimeout`.
128128
129129 ## Bon à savoir
130130
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.
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 les deuxièmes avis.
132+- **Une seule fois** : le modèle de Bob (4B) a parfois relancé la même question ; son prompt lui dit d'appeler `ask_agent` une seule fois par tâche.
133+- **Anglais** : ces modèles répondent mieux aux questions en anglais.
134134 - **Pas de sessions chez les agents appelés** : ils tournent avec `AGENT_SESSIONS=off`, donc vos `/sessions` ne listent que celles de Bob.
135135 - **Profondeur limitée** : Bob peut interroger Jan, Jan peut interroger un autre agent, mais ce dernier ne peut plus interroger personne (`MM_ASK_DEPTH`).
136136 - **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+- **Les skills de délégation** (`delegate-to-jan`, `second-opinion`, `compare-agents`) qui faisaient lancer `mm --ask` à Bob ont été retirées au profit d'`ask_agent` ; elles restent dans l'historique git (commit 8941766) et le guide sur les skills en montre une.
137138
138139 ## Pour aller plus loin
139140
140141 - [Comment interroger un autre agent mm depuis votre session](../docs/fr/how-to/ask-another-agent.md)
142+- [Comment laisser votre agent appeler d'autres agents](../docs/fr/how-to/let-the-agent-call-other-agents.md)
141143 - [Comment interroger un autre agent depuis une skill ou un script](../docs/fr/how-to/ask-another-agent-from-a-skill.md)
142144 - [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)
143145 - [Pourquoi c'est construit ainsi](../docs/fr/explanation/asking-another-agent.md)
@@ -6,22 +6,26 @@ Ce dossier montre comment un agent mini-me (Bob) peut poser des questions à d'a
6 6
7 | Fichier | Rôle |7 | Fichier | Rôle |
8 |---|---|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/`. |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`, avec une description et, pour Lucy, son propre `yolo`. |
10 | `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), agent complet avec ses outils. |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. |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. |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). |13 | `skills/bg-jobs` | Copie de la skill générale (le prompt de Bob y fait référence). |
17 14
18 Dans `agent.llmman.yaml` :15 Dans `agent.llmman.yaml` :
19 16
20 ```yaml17 ```yaml
21 mmConfigs:18 mmConfigs:
22- jan: agent.jan-nano.yaml19+ jan:
23- lucy: agent.lucy.yaml20+ path: agent.jan-nano.yaml
24- reviewer: agent.lucy-reviewer.yaml21+ description: Research and fact questions — looks things up and answers in a few lines.
22+ lucy:
23+ path: agent.lucy.yaml
24+ description: A coding agent — reads, writes and runs code in this project. …
25+ yolo: true
26+ reviewer:
27+ path: agent.lucy-reviewer.yaml
28+ description: A second opinion on an answer or a claim from this conversation — call it with with_context true. No tools.
25 ```29 ```
26 30
27 ## Prérequis31 ## Prérequis
@@ -86,58 +90,56 @@ printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, P
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.90 - 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.91 - **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.92 - 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`.93+- Les appels d'outils de l'agent appelé suivent le `yolo` de son entrée dans `mmConfigs` (Lucy : `true`, ils s'exécutent), sinon celui de `agent.llmman.yaml` (`false` : refusés, personne ne peut répondre). Pour un agent qui doit seulement répondre, utilisez `reviewer`.
90 94
91-## 3. Laisser Bob interroger les autres via ses skills95+## 3. Laisser Bob décider : l'outil `ask_agent`
92 96
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 :97+Comme `mmConfigs` n'est pas vide, Bob dispose de l'outil intégré `ask_agent`. Sa description liste Jan, Lucy et le relecteur avec leur `description` : c'est Bob qui choisit, sans skill. Demandez en langage courant :
94 98
95-| Vous dites | Skill | Commande lancée par Bob |99+| Vous dites | Bob appelle |
96-|---|---|---|100+|---|---|
97-| « Ask Jan what the capital of Peru is. » | `delegate-to-jan` | `"$MM_BIN" --ask jan -p "What is the capital of Peru?"` |101+| « Ask Jan what the capital of Peru is. » | `ask_agent` → `jan` |
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' …` |102+| « I think Go's goroutines are OS threads. Get a second opinion on that claim. » | `ask_agent` → `reviewer`, avec `with_context` |
99-| « Ask both Jan and Lucy: what is a closure in Go? » | `compare-agents` | les deux `--ask` dans une seule commande |103+| « Ask Lucy to add a test for main.go and run it. » | `ask_agent` → `lucy`, dont les écritures et commandes s'exécutent sans demander (`yolo: true` dans son entrée) |
100 104
101-Exemple réel (`second-opinion`, 97 s au total) :105+Bob a `yolo: false` : vous approuvez d'abord chaque passage de main, avec le prompt qu'il a écrit. Dans la fenêtre (`../mm -tui agent.llmman.yaml`), l'agent appelé est nommé pendant qu'il travaille :
102 106
103 ```text107 ```text
104-📖 read_skill: second-opinion108+mm [Jan (coding agent)] · Jan-nano:Q4_K_M @ llmman · asked by mm [Bob (coordinator)]
105-🛠️ bash: "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'109+…
106-User: I think Go's goroutines are OS threads.110+‣ mm [Jan (coding agent)]
107-Assistant: Actually, Go goroutines are not OS threads. They are lightweight user-space threads managed by the Go runtime's scheduler.111+The capital of Peru is Lima.
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 ```112 ```
115 113
116-`compare-agents` n'a pas encore été testée en conditions réelles.114+puis l'en-tête revient à Bob, qui conclut sous `‣ mm [Bob (coordinator)]`. Mesuré : 6 s pour Jan, 5,6 s pour le relecteur (`mm -p`, modèles déjà chargés).
115+
116+La réponse revient à Bob comme résultat de l'outil, dans le même tour ; elle n'est pas ajoutée à l'historique comme avec `/ask`.
117 117
118 ## Réglages de temps118 ## Réglages de temps
119 119
120 Un petit modèle local peut mettre une minute à répondre. `agent.llmman.yaml` relève donc deux délais :120 Un petit modèle local peut mettre une minute à répondre. `agent.llmman.yaml` relève donc deux délais :
121 121
122 ```yaml122 ```yaml
123-watchdogTimeout: 3m # le watchdog continue de compter pendant qu'une commande tourne (ticket 57)123+watchdogTimeout: 3m # silence toléré ; pendant ask_agent, chaque événement de l'agent appelé compte comme signe de vie
124-bashTimeout: 3m # durée maximale d'une commande bash (30s par défaut)124+bashTimeout: 3m # durée maximale d'une commande bash (30s par défaut), pour un script qui lance mm --ask
125 ```125 ```
126 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`.127+Avec `ask_agent`, seul un long silence de l'agent appelé (le chargement de son modèle, la lecture d'un long premier prompt) peut encore dépasser `watchdogTimeout`.
128 128
129 ## Bon à savoir129 ## Bon à savoir
130 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`.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 les deuxièmes avis.
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.132+- **Une seule fois** : le modèle de Bob (4B) a parfois relancé la même question ; son prompt lui dit d'appeler `ask_agent` une seule fois par tâche.
133-- **Anglais** : ces modèles répondent mieux aux questions en anglais ; les skills leur font écrire la question en anglais.133+- **Anglais** : ces modèles répondent mieux aux questions 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.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`).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.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+- **Les skills de délégation** (`delegate-to-jan`, `second-opinion`, `compare-agents`) qui faisaient lancer `mm --ask` à Bob ont été retirées au profit d'`ask_agent` ; elles restent dans l'historique git (commit 8941766) et le guide sur les skills en montre une.
137 138
138 ## Pour aller plus loin139 ## Pour aller plus loin
139 140
140 - [Comment interroger un autre agent mm depuis votre session](../docs/fr/how-to/ask-another-agent.md)141 - [Comment interroger un autre agent mm depuis votre session](../docs/fr/how-to/ask-another-agent.md)
142+- [Comment laisser votre agent appeler d'autres agents](../docs/fr/how-to/let-the-agent-call-other-agents.md)
141 - [Comment interroger un autre agent depuis une skill ou un script](../docs/fr/how-to/ask-another-agent-from-a-skill.md)143 - [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)144 - [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)145 - [Pourquoi c'est construit ainsi](../docs/fr/explanation/asking-another-agent.md)
modified demo-llmman/agent.llmman.yaml +41 -14
@@ -17,18 +17,37 @@ baseUrl: http://127.0.0.1:17434/v1
1717 # From a Docker sandbox, the daemon runs on the host.
1818 fallback: http://host.docker.internal:17434/v1
1919
20-# Other mm agents this one can ask. Each name becomes /ask-<name> and
21-# /ask-with-context-<name> (also `/ask <name> <prompt>` in the terminal and the
22-# window); the answer is shown and kept in the history for the next question.
23-# Each agent runs as a child `mm -acp` started on its first /ask. Paths are
20+# Other mm agents this one can ask. Two ways:
21+# - YOU type /ask-<name> or /ask-with-context-<name> (also `/ask <name> …`);
22+# the answer is shown and kept in the history for the next question;
23+# - BOB decides, with the `ask_agent` tool (declared because this list is
24+# not empty): he picks an agent from its `description`, the agent works
25+# in front of you (the window names it in its header and above its
26+# answer), and its answer comes back to Bob as the tool's result.
27+# Each agent runs as a child `mm -acp` started on its first ask. Paths are
2428 # relative to THIS file's directory. Names: lowercase letters, digits and '-'.
29+#
30+# An entry is a path, or {path, description, yolo}:
31+# description when to call this agent — what ask_agent shows Bob.
32+# yolo decides THIS agent's tool calls instead of Bob's yolo:
33+# true = they run, false = each one is asked. Absent = Bob's
34+# yolo decides (false here: you approve each call).
2535 mmConfigs:
26- jan: agent.jan-nano.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
36+ jan:
37+ path: agent.jan-nano.yaml
38+ description: Research and fact questions — looks things up and answers in a few lines.
39+ lucy:
40+ path: agent.lucy.yaml
41+ description: A coding agent — reads, writes and runs code in this project. Give it the files to change and the check to run.
42+ # Lucy's writes and commands run without asking once you approved the
43+ # hand-off: coding is her job.
44+ yolo: true
45+ # Lucy's model with no tools and a reviewer prompt. A small model with
46+ # every tool wanders (reads files, loads skills) when it is only asked for
47+ # an opinion.
48+ reviewer:
49+ path: agent.lucy-reviewer.yaml
50+ description: A second opinion on an answer or a claim from this conversation — call it with with_context true. No tools.
3251
3352 acp:
3453 name: bob
@@ -143,6 +162,14 @@ system: |
143162 you invent. Match the request against that list, call `read_skill` FIRST,
144163 before any bash command, and then follow what it says step by step.
145164
165+ OTHER AGENTS
166+ `ask_agent` hands a task to another agent; its description says which agent
167+ does what. Use it when the user names an agent, asks for a second opinion,
168+ or asks for something an agent there is for. Give it a complete prompt —
169+ it does not see this conversation unless with_context is true. Call it ONCE
170+ per task: its result is that agent's answer. Then answer the user, saying
171+ which agent answered and what it said; never answer in its place.
172+
146173 FILE EDITING
147174 You have three tools for files: `read_file`, `edit_file` and `write_file`.
148175 They are how a file gets read and changed here: each change is exact,
@@ -201,10 +228,10 @@ sampling:
201228 # Silence allowed between two tokens once the answer has started.
202229 watchdogTimeout: 3m
203230
204-# How long one bash command may run (default 30s). The demo skills call jan and
205-# lucy with "$MM_BIN" --ask, which can take a minute or two with small local
206-# models. The watchdog above keeps counting while a command runs (ticket 57),
207-# so it is raised to the same value.
231+# How long one bash command may run (default 30s). The watchdog above keeps
232+# counting while a command runs (ticket 57), so the two move together.
233+# (ask_agent is not bash: the asked agent's activity keeps Bob's watchdog
234+# alive, and its own watchdog stops it if it hangs.)
208235 bashTimeout: 3m
209236
210237 # Silence allowed while the model reads the prompt, before the first token:
@@ -17,18 +17,37 @@ baseUrl: http://127.0.0.1:17434/v1
17 # From a Docker sandbox, the daemon runs on the host.17 # From a Docker sandbox, the daemon runs on the host.
18 fallback: http://host.docker.internal:17434/v118 fallback: http://host.docker.internal:17434/v1
19 19
20-# Other mm agents this one can ask. Each name becomes /ask-<name> and20+# Other mm agents this one can ask. Two ways:
21-# /ask-with-context-<name> (also `/ask <name> <prompt>` in the terminal and the21+# - YOU type /ask-<name> or /ask-with-context-<name> (also `/ask <name> …`);
22-# window); the answer is shown and kept in the history for the next question.22+# the answer is shown and kept in the history for the next question;
23-# Each agent runs as a child `mm -acp` started on its first /ask. Paths are23+# - BOB decides, with the `ask_agent` tool (declared because this list is
24+# not empty): he picks an agent from its `description`, the agent works
25+# in front of you (the window names it in its header and above its
26+# answer), and its answer comes back to Bob as the tool's result.
27+# Each agent runs as a child `mm -acp` started on its first ask. Paths are
24 # relative to THIS file's directory. Names: lowercase letters, digits and '-'.28 # relative to THIS file's directory. Names: lowercase letters, digits and '-'.
29+#
30+# An entry is a path, or {path, description, yolo}:
31+# description when to call this agent — what ask_agent shows Bob.
32+# yolo decides THIS agent's tool calls instead of Bob's yolo:
33+# true = they run, false = each one is asked. Absent = Bob's
34+# yolo decides (false here: you approve each call).
25 mmConfigs:35 mmConfigs:
26- jan: agent.jan-nano.yaml36+ jan:
27- lucy: agent.lucy.yaml37+ path: agent.jan-nano.yaml
28- # Lucy's model with no tools and a reviewer prompt: what the second-opinion38+ description: Research and fact questions — looks things up and answers in a few lines.
29- # skill asks. A small model with every tool wanders (reads files, loads39+ lucy:
30- # skills) when it is only asked for an opinion.40+ path: agent.lucy.yaml
31- reviewer: agent.lucy-reviewer.yaml41+ description: A coding agent — reads, writes and runs code in this project. Give it the files to change and the check to run.
42+ # Lucy's writes and commands run without asking once you approved the
43+ # hand-off: coding is her job.
44+ yolo: true
45+ # Lucy's model with no tools and a reviewer prompt. A small model with
46+ # every tool wanders (reads files, loads skills) when it is only asked for
47+ # an opinion.
48+ reviewer:
49+ path: agent.lucy-reviewer.yaml
50+ description: A second opinion on an answer or a claim from this conversation — call it with with_context true. No tools.
32 51
33 acp:52 acp:
34 name: bob53 name: bob
@@ -143,6 +162,14 @@ system: |
143 you invent. Match the request against that list, call `read_skill` FIRST,162 you invent. Match the request against that list, call `read_skill` FIRST,
144 before any bash command, and then follow what it says step by step.163 before any bash command, and then follow what it says step by step.
145 164
165+ OTHER AGENTS
166+ `ask_agent` hands a task to another agent; its description says which agent
167+ does what. Use it when the user names an agent, asks for a second opinion,
168+ or asks for something an agent there is for. Give it a complete prompt —
169+ it does not see this conversation unless with_context is true. Call it ONCE
170+ per task: its result is that agent's answer. Then answer the user, saying
171+ which agent answered and what it said; never answer in its place.
172+
146 FILE EDITING173 FILE EDITING
147 You have three tools for files: `read_file`, `edit_file` and `write_file`.174 You have three tools for files: `read_file`, `edit_file` and `write_file`.
148 They are how a file gets read and changed here: each change is exact,175 They are how a file gets read and changed here: each change is exact,
@@ -201,10 +228,10 @@ sampling:
201 # Silence allowed between two tokens once the answer has started.228 # Silence allowed between two tokens once the answer has started.
202 watchdogTimeout: 3m229 watchdogTimeout: 3m
203 230
204-# How long one bash command may run (default 30s). The demo skills call jan and231+# How long one bash command may run (default 30s). The watchdog above keeps
205-# lucy with "$MM_BIN" --ask, which can take a minute or two with small local232+# counting while a command runs (ticket 57), so the two move together.
206-# models. The watchdog above keeps counting while a command runs (ticket 57),233+# (ask_agent is not bash: the asked agent's activity keeps Bob's watchdog
207-# so it is raised to the same value.234+# alive, and its own watchdog stops it if it hangs.)
208 bashTimeout: 3m235 bashTimeout: 3m
209 236
210 # Silence allowed while the model reads the prompt, before the first token:237 # Silence allowed while the model reads the prompt, before the first token:
deleted demo-llmman/skills/compare-agents/SKILL.md +0 -23
deleted file mode 100644
@@ -1,23 +0,0 @@
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.
deleted file mode 100644
@@ -1,23 +0,0 @@
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.
deleted demo-llmman/skills/delegate-to-jan/SKILL.md +0 -23
deleted file mode 100644
@@ -1,23 +0,0 @@
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.
deleted file mode 100644
@@ -1,23 +0,0 @@
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.
deleted demo-llmman/skills/second-opinion/SKILL.md +0 -28
deleted file mode 100644
@@ -1,28 +0,0 @@
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.
deleted file mode 100644
@@ -1,28 +0,0 @@
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.
modified docs/en/README.md +3 -2
@@ -14,7 +14,8 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr)
1414 - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demo-llmman/`
1515 - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demo-ollama/`
1616 - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>`
17-- [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, the demo skills
17+- [Let your agent call other agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, `mmConfigs` descriptions, per-agent `yolo`
18+- [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`
1819 - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md)
1920 - [Turn a model's reasoning off](how-to/turn-reasoning-off.md) — `chat_template_kwargs` or `reasoning_effort` in `sampling`, per server
2021 - [Confirm tool calls before they run](how-to/confirm-tool-calls.md) — `yolo: false`, the four answers, `AGENT_YOLO`, who decides with `mmConfigs`
@@ -61,7 +62,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr)
6162 - [The `fetch_content` tool](explanation/fetch-content-tool.md) — why a built-in tool beside MCP `fetch`, a home-made converter, robots.txt, errors as numbered text, known limits
6263 - [Thinking models](explanation/thinking-models.md) — where a model's reasoning comes from, why `mm` reads it below Genkit, and why reading it keeps the watchdog alive
6364 - [Yolo mode and permissions](explanation/yolo-and-permissions.md) — why tools run unasked by default, existing versus asking, four answers in one package, why the asking agent decides
64-- [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, the depth limit
65+- [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, `ask_agent` and per-agent `yolo`, the depth limit
6566 - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio)
6667
6768 ### MCP (Model Context Protocol) Explanation
@@ -14,7 +14,8 @@ 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 skills17+- [Let your agent call other agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, `mmConfigs` descriptions, per-agent `yolo`
18+- [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`
18 - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md)19 - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md)
19 - [Turn a model's reasoning off](how-to/turn-reasoning-off.md) — `chat_template_kwargs` or `reasoning_effort` in `sampling`, per server20 - [Turn a model's reasoning off](how-to/turn-reasoning-off.md) — `chat_template_kwargs` or `reasoning_effort` in `sampling`, per server
20 - [Confirm tool calls before they run](how-to/confirm-tool-calls.md) — `yolo: false`, the four answers, `AGENT_YOLO`, who decides with `mmConfigs`21 - [Confirm tool calls before they run](how-to/confirm-tool-calls.md) — `yolo: false`, the four answers, `AGENT_YOLO`, who decides with `mmConfigs`
@@ -61,7 +62,7 @@ This documentation is organised along the four [Diátaxis](https://diataxis.fr)
61 - [The `fetch_content` tool](explanation/fetch-content-tool.md) — why a built-in tool beside MCP `fetch`, a home-made converter, robots.txt, errors as numbered text, known limits62 - [The `fetch_content` tool](explanation/fetch-content-tool.md) — why a built-in tool beside MCP `fetch`, a home-made converter, robots.txt, errors as numbered text, known limits
62 - [Thinking models](explanation/thinking-models.md) — where a model's reasoning comes from, why `mm` reads it below Genkit, and why reading it keeps the watchdog alive63 - [Thinking models](explanation/thinking-models.md) — where a model's reasoning comes from, why `mm` reads it below Genkit, and why reading it keeps the watchdog alive
63 - [Yolo mode and permissions](explanation/yolo-and-permissions.md) — why tools run unasked by default, existing versus asking, four answers in one package, why the asking agent decides64 - [Yolo mode and permissions](explanation/yolo-and-permissions.md) — why tools run unasked by default, existing versus asking, four answers in one package, why the asking agent decides
64-- [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, the depth limit65+- [Asking another agent](explanation/asking-another-agent.md) — why another mm agent is a child process spoken to over ACP, the history pair, fresh sessions, `ask_agent` and per-agent `yolo`, the depth limit
65 - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio)66 - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio)
66 67
67 ### MCP (Model Context Protocol) Explanation68 ### MCP (Model Context Protocol) Explanation
modified docs/en/explanation/asking-another-agent.md +7 -4
@@ -2,7 +2,7 @@
22
33 ## What is this about?
44
5-`/ask-bob <prompt>` lets the agent you are talking to consult another mm agent: another model, another system prompt, other tools and skills. Bob's answer is shown, then kept in your conversation so your own model can use it on your next question. This page explains how that is built and why.
5+`/ask-bob <prompt>` lets the agent you are talking to consult another mm agent: another model, another system prompt, other tools and skills. Bob's answer is shown, then kept in your conversation so your own model can use it on your next question. The `ask_agent` tool lets your model do the same by itself, when a request matches what an agent is for. This page explains how that is built and why.
66
77 ## Why it is designed this way
88
@@ -10,7 +10,7 @@
1010
1111 **The two talk ACP.** mm already speaks the Agent Client Protocol as a server, for editors. Asking bob makes the asking agent an ACP *client* of its child. The protocol gives, for free, what scraping a terminal never would: bob's answer as text, his tool calls as structured events, his permission requests as requests that can be answered, and a standard way to cancel. mm's ACP server only ever calls two client methods — session updates and permission requests — so the client side is small; file and terminal requests are refused as absent.
1212
13-**Bob's permissions are yours to give.** Bob is started with `AGENT_YOLO=false`, so every call he would gate becomes a permission request to your agent, and YOUR `yolo` decides: on, it runs; off, the question goes to the front end you are using — the editor's dialog, the window's box, the line mode's `🔐` prompt — exactly as for your own agent's tools there. The `yolo` in bob's own file does not count while he answers you. Bob's tools are named `bob/<tool>`, so allowing your agent's `bash` "for this session" does not allow bob's. See [Yolo mode and permissions](yolo-and-permissions.md).
13+**Bob's permissions are yours to give.** Bob is started with `AGENT_YOLO=false`, so every call he would gate becomes a permission request to your agent, and YOUR `yolo` decides: on, it runs; off, the question goes to the front end you are using — the editor's dialog, the window's box, the line mode's `🔐` prompt — exactly as for your own agent's tools there. The `yolo` in bob's own file does not count while he answers you — the one in YOUR file's entry for him does: `mmConfigs: {bob: {path: …, yolo: true}}` lets bob's calls run while yours still ask (a coder you trust with edits once you approved the hand-off), `yolo: false` asks for each of his calls even when yours run freely. Bob's tools are named `bob/<tool>`, so allowing your agent's `bash` "for this session" does not allow bob's. See [Yolo mode and permissions](yolo-and-permissions.md).
1414
1515 **The answer is a pair of messages.** It enters your history as a user message (`[asked bob (provider/model): …]`) followed by an assistant message (`[bob's answer]` and the text). One message would have been simpler, but your next question is a user message too, and several chat templates (Gemma's, Mistral's) refuse two user messages in a row. The marks say who said what, so your model does not mistake bob's answer for its own. The answer is capped like a tool's output, at `maxOutput` characters: it lands in your model's window.
1616
@@ -20,11 +20,13 @@
2020
2121 **The child is fenced off from what is meant for its parent.** Variables like `AGENT_MODEL`, `AGENT_PROVIDER` or `AGENT_BASE_URL` override a configuration; inherited by bob, they would silently turn him into a copy of you. They are removed from his environment. Bob also runs with `AGENT_SESSIONS=off`: he works in your project, and his session files would otherwise appear in your `/sessions` and `/resume`.
2222
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+**Your model can decide too: `ask_agent`.** With `mmConfigs` set, your agent gets a built-in tool that asks one of the agents and returns its answer as the tool's result. Its description lists the agents with what each is for — the entry's `description`, else the agent's `acp.title` — and that is all the model has to choose from, so a description says when to call the agent, not who it is. The tool runs on the same pool and protocol as `/ask`, inside the turn: the agent's text, thoughts and tool calls stream through your front end as they happen, and the window names it while it works — its title and model in the header, `asked by` your agent, its answer labelled with its name. The call itself is gated like `bash`: with `yolo: false` you see the prompt your model wrote before it goes. The answer is a tool result, not a `/ask` pair in the history: your model reads it and carries on in the same turn — report it, check it, hand the next step to another agent.
24+
25+**Chains stop.** A model calling `ask_agent` from an agent that was itself asked could go on asking; each child is told its depth (`MM_ASK_DEPTH`): your agent may ask bob, bob may ask riker, riker may not ask anyone — whether by a typed `/ask` or by `ask_agent`.
2426
2527 **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 follow the calling configuration's `yolo`: allowed when it is on — the person already approved the `mm --ask` command itself —, rejected when it is off, since stdin carried the question and nobody is there to answer. 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.
2628
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.
29+**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. `ask_agent` does better because it sees the agent work: every event the agent streams — text, a thought, a tool call — counts as a sign of life for your agent's watchdog. Only a long silence still ends the turn: an agent loading a large model, or reading a long first prompt, so `watchdogTimeout` must cover that much.
2830
2931 ## Rejected alternatives
3032
@@ -32,6 +34,7 @@
3234 - **A second engine in the same process.** Fast, no process — at the price of turning the global configuration into a per-agent one throughout the code base.
3335 - **Letting the current model answer at once with bob's input.** The user chose otherwise: bob's answer is shown and kept, and you decide what to ask next.
3436 - **Keeping bob's own conversation across asks.** Two conversations evolving in parallel, one of them invisible, are harder to reason about than an explicit transcript sent when asked.
37+- **Skills that run `mm --ask` for the model.** It works — a skill per hand-off, the command in its text — and `mm --ask` stays for scripts. But the agent's work reaches you as the output of one `bash` command: no name in the header, no label on its answer, its tool calls as text lines. And a small coordinator must copy a long command exactly: measured with Gemma 4 E4B, it dropped the quotes and an `AGENT_YOLO=true` prefix, so the coder it called could not write. A tool call with an `agent` and a `prompt` has nothing to copy.
3538 - **A generic `/ask <name>` only, in editors too.** An editor's `/` menu completes a command name, never its argument; one command per agent is the only way for the menu to list the agents.
3639
3740 ## How it relates to the rest
@@ -2,7 +2,7 @@
2 2
3 ## What is this about?3 ## What is this about?
4 4
5-`/ask-bob <prompt>` lets the agent you are talking to consult another mm agent: another model, another system prompt, other tools and skills. Bob's answer is shown, then kept in your conversation so your own model can use it on your next question. This page explains how that is built and why.5+`/ask-bob <prompt>` lets the agent you are talking to consult another mm agent: another model, another system prompt, other tools and skills. Bob's answer is shown, then kept in your conversation so your own model can use it on your next question. The `ask_agent` tool lets your model do the same by itself, when a request matches what an agent is for. This page explains how that is built and why.
6 6
7 ## Why it is designed this way7 ## Why it is designed this way
8 8
@@ -10,7 +10,7 @@
10 10
11 **The two talk ACP.** mm already speaks the Agent Client Protocol as a server, for editors. Asking bob makes the asking agent an ACP *client* of its child. The protocol gives, for free, what scraping a terminal never would: bob's answer as text, his tool calls as structured events, his permission requests as requests that can be answered, and a standard way to cancel. mm's ACP server only ever calls two client methods — session updates and permission requests — so the client side is small; file and terminal requests are refused as absent.11 **The two talk ACP.** mm already speaks the Agent Client Protocol as a server, for editors. Asking bob makes the asking agent an ACP *client* of its child. The protocol gives, for free, what scraping a terminal never would: bob's answer as text, his tool calls as structured events, his permission requests as requests that can be answered, and a standard way to cancel. mm's ACP server only ever calls two client methods — session updates and permission requests — so the client side is small; file and terminal requests are refused as absent.
12 12
13-**Bob's permissions are yours to give.** Bob is started with `AGENT_YOLO=false`, so every call he would gate becomes a permission request to your agent, and YOUR `yolo` decides: on, it runs; off, the question goes to the front end you are using — the editor's dialog, the window's box, the line mode's `🔐` prompt — exactly as for your own agent's tools there. The `yolo` in bob's own file does not count while he answers you. Bob's tools are named `bob/<tool>`, so allowing your agent's `bash` "for this session" does not allow bob's. See [Yolo mode and permissions](yolo-and-permissions.md).13+**Bob's permissions are yours to give.** Bob is started with `AGENT_YOLO=false`, so every call he would gate becomes a permission request to your agent, and YOUR `yolo` decides: on, it runs; off, the question goes to the front end you are using — the editor's dialog, the window's box, the line mode's `🔐` prompt — exactly as for your own agent's tools there. The `yolo` in bob's own file does not count while he answers you — the one in YOUR file's entry for him does: `mmConfigs: {bob: {path: …, yolo: true}}` lets bob's calls run while yours still ask (a coder you trust with edits once you approved the hand-off), `yolo: false` asks for each of his calls even when yours run freely. Bob's tools are named `bob/<tool>`, so allowing your agent's `bash` "for this session" does not allow bob's. See [Yolo mode and permissions](yolo-and-permissions.md).
14 14
15 **The answer is a pair of messages.** It enters your history as a user message (`[asked bob (provider/model): …]`) followed by an assistant message (`[bob's answer]` and the text). One message would have been simpler, but your next question is a user message too, and several chat templates (Gemma's, Mistral's) refuse two user messages in a row. The marks say who said what, so your model does not mistake bob's answer for its own. The answer is capped like a tool's output, at `maxOutput` characters: it lands in your model's window.15 **The answer is a pair of messages.** It enters your history as a user message (`[asked bob (provider/model): …]`) followed by an assistant message (`[bob's answer]` and the text). One message would have been simpler, but your next question is a user message too, and several chat templates (Gemma's, Mistral's) refuse two user messages in a row. The marks say who said what, so your model does not mistake bob's answer for its own. The answer is capped like a tool's output, at `maxOutput` characters: it lands in your model's window.
16 16
@@ -20,11 +20,13 @@
20 20
21 **The child is fenced off from what is meant for its parent.** Variables like `AGENT_MODEL`, `AGENT_PROVIDER` or `AGENT_BASE_URL` override a configuration; inherited by bob, they would silently turn him into a copy of you. They are removed from his environment. Bob also runs with `AGENT_SESSIONS=off`: he works in your project, and his session files would otherwise appear in your `/sessions` and `/resume`.21 **The child is fenced off from what is meant for its parent.** Variables like `AGENT_MODEL`, `AGENT_PROVIDER` or `AGENT_BASE_URL` override a configuration; inherited by bob, they would silently turn him into a copy of you. They are removed from his environment. Bob also runs with `AGENT_SESSIONS=off`: he works in your project, and his session files would otherwise appear in your `/sessions` and `/resume`.
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+**Your model can decide too: `ask_agent`.** With `mmConfigs` set, your agent gets a built-in tool that asks one of the agents and returns its answer as the tool's result. Its description lists the agents with what each is for — the entry's `description`, else the agent's `acp.title` — and that is all the model has to choose from, so a description says when to call the agent, not who it is. The tool runs on the same pool and protocol as `/ask`, inside the turn: the agent's text, thoughts and tool calls stream through your front end as they happen, and the window names it while it works — its title and model in the header, `asked by` your agent, its answer labelled with its name. The call itself is gated like `bash`: with `yolo: false` you see the prompt your model wrote before it goes. The answer is a tool result, not a `/ask` pair in the history: your model reads it and carries on in the same turn — report it, check it, hand the next step to another agent.
24+
25+**Chains stop.** A model calling `ask_agent` from an agent that was itself asked could go on asking; each child is told its depth (`MM_ASK_DEPTH`): your agent may ask bob, bob may ask riker, riker may not ask anyone — whether by a typed `/ask` or by `ask_agent`.
24 26
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 follow the calling configuration's `yolo`: allowed when it is on — the person already approved the `mm --ask` command itself —, rejected when it is off, since stdin carried the question and nobody is there to answer. 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.27 **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 follow the calling configuration's `yolo`: allowed when it is on — the person already approved the `mm --ask` command itself —, rejected when it is off, since stdin carried the question and nobody is there to answer. 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 28
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.29+**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. `ask_agent` does better because it sees the agent work: every event the agent streams — text, a thought, a tool call — counts as a sign of life for your agent's watchdog. Only a long silence still ends the turn: an agent loading a large model, or reading a long first prompt, so `watchdogTimeout` must cover that much.
28 30
29 ## Rejected alternatives31 ## Rejected alternatives
30 32
@@ -32,6 +34,7 @@
32 - **A second engine in the same process.** Fast, no process — at the price of turning the global configuration into a per-agent one throughout the code base.34 - **A second engine in the same process.** Fast, no process — at the price of turning the global configuration into a per-agent one throughout the code base.
33 - **Letting the current model answer at once with bob's input.** The user chose otherwise: bob's answer is shown and kept, and you decide what to ask next.35 - **Letting the current model answer at once with bob's input.** The user chose otherwise: bob's answer is shown and kept, and you decide what to ask next.
34 - **Keeping bob's own conversation across asks.** Two conversations evolving in parallel, one of them invisible, are harder to reason about than an explicit transcript sent when asked.36 - **Keeping bob's own conversation across asks.** Two conversations evolving in parallel, one of them invisible, are harder to reason about than an explicit transcript sent when asked.
37+- **Skills that run `mm --ask` for the model.** It works — a skill per hand-off, the command in its text — and `mm --ask` stays for scripts. But the agent's work reaches you as the output of one `bash` command: no name in the header, no label on its answer, its tool calls as text lines. And a small coordinator must copy a long command exactly: measured with Gemma 4 E4B, it dropped the quotes and an `AGENT_YOLO=true` prefix, so the coder it called could not write. A tool call with an `agent` and a `prompt` has nothing to copy.
35 - **A generic `/ask <name>` only, in editors too.** An editor's `/` menu completes a command name, never its argument; one command per agent is the only way for the menu to list the agents.38 - **A generic `/ask <name>` only, in editors too.** An editor's `/` menu completes a command name, never its argument; one command per agent is the only way for the menu to list the agents.
36 39
37 ## How it relates to the rest40 ## How it relates to the rest
modified docs/en/how-to/ask-another-agent-from-a-skill.md +24 -5
@@ -1,6 +1,6 @@
11 # How to ask another agent from a skill or a script
22
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)).
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)). For your model to hand work to another agent on its own, the [`ask_agent` tool](let-the-agent-call-other-agents.md) is the simpler way: no command to copy, and the window shows which agent is working. A skill is for a fixed procedure, a script, or several agents in one command.
44
55 ## Steps
66
@@ -25,11 +25,30 @@ This guide shows how to make a skill — or any shell script — put a question
2525
2626 The model writes this summary itself: a command cannot read the conversation.
2727
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:
28+3. Tell the model, in the skill, to run the command once and to report the answer as the other agent's. A complete skill (`skills/second-opinion/SKILL.md`):
2929
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.
30+ ```markdown
31+ ---
32+ name: second-opinion
33+ description: Get a second opinion from the reviewer agent on a claim or an answer from this conversation.
34+ keywords: second opinion, review, check, deuxième avis, vérifie, relis
35+ ---
36+
37+ # Second opinion
38+
39+ Call your `bash` tool ONCE with this command, the conversation summarised in English above the `---` line, the question below it:
40+
41+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
42+ User: <the claim>
43+ Assistant: <your answer, if any>
44+ ---
45+ Is this right? Correct it if not.
46+ CONTEXT
47+
48+ What it prints is the reviewer's answer. Report it, starting with "The reviewer says:"; never answer in its place. If it prints `[ask: …]`, tell the user that line.
49+ ```
50+
51+ Keep such a command short: a small model copies a long one inexactly (quotes, prefixes).
3352
3453 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:
3554
@@ -1,6 +1,6 @@
1 # How to ask another agent from a skill or a script1 # How to ask another agent from a skill or a script
2 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)).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)). For your model to hand work to another agent on its own, the [`ask_agent` tool](let-the-agent-call-other-agents.md) is the simpler way: no command to copy, and the window shows which agent is working. A skill is for a fixed procedure, a script, or several agents in one command.
4 4
5 ## Steps5 ## Steps
6 6
@@ -25,11 +25,30 @@ This guide shows how to make a skill — or any shell script — put a question
25 25
26 The model writes this summary itself: a command cannot read the conversation.26 The model writes this summary itself: a command cannot read the conversation.
27 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:28+3. Tell the model, in the skill, to run the command once and to report the answer as the other agent's. A complete skill (`skills/second-opinion/SKILL.md`):
29 29
30- - `delegate-to-jan` — a question delegated to Jan;30+ ```markdown
31- - `second-opinion` — the conversation sent to a reviewer;31+ ---
32- - `compare-agents` — the same question to Jan and Lucy, in one command.32+ name: second-opinion
33+ description: Get a second opinion from the reviewer agent on a claim or an answer from this conversation.
34+ keywords: second opinion, review, check, deuxième avis, vérifie, relis
35+ ---
36+
37+ # Second opinion
38+
39+ Call your `bash` tool ONCE with this command, the conversation summarised in English above the `---` line, the question below it:
40+
41+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
42+ User: <the claim>
43+ Assistant: <your answer, if any>
44+ ---
45+ Is this right? Correct it if not.
46+ CONTEXT
47+
48+ What it prints is the reviewer's answer. Report it, starting with "The reviewer says:"; never answer in its place. If it prints `[ask: …]`, tell the user that line.
49+ ```
50+
51+ Keep such a command short: a small model copies a long one inexactly (quotes, prefixes).
33 52
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:53 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 54
added docs/en/how-to/let-the-agent-call-other-agents.md +73 -0
new file mode 100644
@@ -0,0 +1,73 @@
1+# How to let your agent call other agents
2+
3+This guide shows how to give your agent a team: other mm agents it hands work to by itself, with the built-in `ask_agent` tool, while you watch which one is working. It assumes you already run `mm` with a configuration file, and have one configuration file per agent you want it to call.
4+
5+## Steps
6+
7+1. List the agents under `mmConfigs`, each with a `description` that says **when** to call it — the description is all your model has to choose from:
8+
9+ ```yaml
10+ mmConfigs:
11+ milo:
12+ path: agent.milo.yaml
13+ description: Specifier. Turns a request into a specification saved in specs/<slug>.spec.md, and checks code against a specification.
14+ homer:
15+ path: agent.homer.yaml
16+ description: Coder. Implements a specification file from specs/, with tests. Call him only once a specification exists.
17+ ```
18+
19+ Paths are relative to the file you write them in. A plain `name: path` entry still works; it is described by the agent's `acp.title`.
20+
21+2. Decide who approves the agents' own tool calls. By default your agent's `yolo` decides for them too. Give an agent its own `yolo` to change that:
22+
23+ ```yaml
24+ homer:
25+ path: agent.homer.yaml
26+ description: …
27+ yolo: true # his edits and commands run without asking
28+ ```
29+
30+ With your own `yolo: false`, you still approve each hand-off — the `ask_agent` call, with the prompt your model wrote — and then homer works on his own.
31+
32+3. Tell your model, in its `system` prompt, when to hand work over and what to do with the answer. For instance:
33+
34+ ```yaml
35+ system: |
36+ …
37+ OTHER AGENTS
38+ `ask_agent` hands a task to another agent; its description says which
39+ agent does what. Give it a complete prompt. Call it ONCE per task: its
40+ result is that agent's answer. Then answer the user, saying which agent
41+ answered; never answer in its place.
42+ ```
43+
44+4. Start `mm` and check that the banner lists `ask_agent` among the tools:
45+
46+ ```text
47+ mm … | tools: bash, read_skill, read_file, write_file, edit_file, plan, ask_agent | …
48+ ```
49+
50+5. Ask something an agent is for, in plain language. In the window (`mm -tui`), the header switches to the agent while it works, and its answer carries its name:
51+
52+ ```text
53+ mm [Jan (coding agent)] · Jan-nano:Q4_K_M @ llmman · asked by mm [Bob (coordinator)]
54+ …
55+ ‣ mm [Jan (coding agent)]
56+ The capital of Peru is Lima.
57+ ```
58+
59+ Then the header comes back to your agent, which carries on from the answer.
60+
61+## Variants
62+
63+- **Send the conversation along.** The model sets `with_context: true` when the agent needs what was said — a reviewer giving a second opinion. Say so in the agent's description ("call it with with_context true").
64+- **Ask an agent yourself.** `/ask-<name>` and `/ask-with-context-<name>` still work, and keep the exchange in the history. See [How to ask another mm agent](ask-another-agent.md).
65+- **Long work.** Each event the agent streams keeps your agent's watchdog alive, but a long silence — a large model loading, a long first prompt — does not: raise your `watchdogTimeout` to cover it (10m for a 48 GB model on a laptop).
66+- **From a script.** `mm --ask <name>` remains the way for a shell command. See [How to ask another agent from a skill or a script](ask-another-agent-from-a-skill.md).
67+- **Working examples.** `demo-llmman/agent.llmman.yaml` (Bob with Jan, Lucy and a reviewer) and `mini-mes/riker/agent.riker.yaml` (a coordinator, a specifier and a coder).
68+
69+## See also
70+
71+- The tool's fields, display and errors: [tools reference](../reference/tools.md#ask_agent)
72+- The two forms of `mmConfigs`: [configuration reference](../reference/configuration.md)
73+- Why it runs inside the turn rather than through a skill: [asking another agent — explanation](../explanation/asking-another-agent.md)
new file mode 100644
@@ -0,0 +1,73 @@
1+# How to let your agent call other agents
2+
3+This guide shows how to give your agent a team: other mm agents it hands work to by itself, with the built-in `ask_agent` tool, while you watch which one is working. It assumes you already run `mm` with a configuration file, and have one configuration file per agent you want it to call.
4+
5+## Steps
6+
7+1. List the agents under `mmConfigs`, each with a `description` that says **when** to call it — the description is all your model has to choose from:
8+
9+ ```yaml
10+ mmConfigs:
11+ milo:
12+ path: agent.milo.yaml
13+ description: Specifier. Turns a request into a specification saved in specs/<slug>.spec.md, and checks code against a specification.
14+ homer:
15+ path: agent.homer.yaml
16+ description: Coder. Implements a specification file from specs/, with tests. Call him only once a specification exists.
17+ ```
18+
19+ Paths are relative to the file you write them in. A plain `name: path` entry still works; it is described by the agent's `acp.title`.
20+
21+2. Decide who approves the agents' own tool calls. By default your agent's `yolo` decides for them too. Give an agent its own `yolo` to change that:
22+
23+ ```yaml
24+ homer:
25+ path: agent.homer.yaml
26+ description: …
27+ yolo: true # his edits and commands run without asking
28+ ```
29+
30+ With your own `yolo: false`, you still approve each hand-off — the `ask_agent` call, with the prompt your model wrote — and then homer works on his own.
31+
32+3. Tell your model, in its `system` prompt, when to hand work over and what to do with the answer. For instance:
33+
34+ ```yaml
35+ system: |
36+ …
37+ OTHER AGENTS
38+ `ask_agent` hands a task to another agent; its description says which
39+ agent does what. Give it a complete prompt. Call it ONCE per task: its
40+ result is that agent's answer. Then answer the user, saying which agent
41+ answered; never answer in its place.
42+ ```
43+
44+4. Start `mm` and check that the banner lists `ask_agent` among the tools:
45+
46+ ```text
47+ mm … | tools: bash, read_skill, read_file, write_file, edit_file, plan, ask_agent | …
48+ ```
49+
50+5. Ask something an agent is for, in plain language. In the window (`mm -tui`), the header switches to the agent while it works, and its answer carries its name:
51+
52+ ```text
53+ mm [Jan (coding agent)] · Jan-nano:Q4_K_M @ llmman · asked by mm [Bob (coordinator)]
54+ …
55+ ‣ mm [Jan (coding agent)]
56+ The capital of Peru is Lima.
57+ ```
58+
59+ Then the header comes back to your agent, which carries on from the answer.
60+
61+## Variants
62+
63+- **Send the conversation along.** The model sets `with_context: true` when the agent needs what was said — a reviewer giving a second opinion. Say so in the agent's description ("call it with with_context true").
64+- **Ask an agent yourself.** `/ask-<name>` and `/ask-with-context-<name>` still work, and keep the exchange in the history. See [How to ask another mm agent](ask-another-agent.md).
65+- **Long work.** Each event the agent streams keeps your agent's watchdog alive, but a long silence — a large model loading, a long first prompt — does not: raise your `watchdogTimeout` to cover it (10m for a 48 GB model on a laptop).
66+- **From a script.** `mm --ask <name>` remains the way for a shell command. See [How to ask another agent from a skill or a script](ask-another-agent-from-a-skill.md).
67+- **Working examples.** `demo-llmman/agent.llmman.yaml` (Bob with Jan, Lucy and a reviewer) and `mini-mes/riker/agent.riker.yaml` (a coordinator, a specifier and a coder).
68+
69+## See also
70+
71+- The tool's fields, display and errors: [tools reference](../reference/tools.md#ask_agent)
72+- The two forms of `mmConfigs`: [configuration reference](../reference/configuration.md)
73+- Why it runs inside the turn rather than through a skill: [asking another agent — explanation](../explanation/asking-another-agent.md)
modified docs/en/reference/configuration.md +2 -2
@@ -33,7 +33,7 @@
3333 | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. |
3434 | `yolo` | bool | `true` | `true`: the tools run without asking. `false`: every call that changes something — `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename`, `plan` `create`/`update`/`next`/`delete` — waits for the user's answer in every front end (line mode, window, editor), and so does every MCP tool, whatever `mcp.permissions` says. Answers: allow once, allow this tool for the session, reject this call, never (this tool, for the session). `read_file`, `read_skill`, `fetch_content` without `output` and the `lsp` navigation actions never ask. In `-p` and `mm --ask`, nobody can answer: those calls are rejected. An agent started by `/ask` runs with `AGENT_YOLO=false` and submits its calls to the asking agent, whose `yolo` decides. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). |
3535 | `permissions` | string | absent | Deprecated, replaced by `yolo`. `ask` = `yolo: false` (and wins over `yolo: true`); `auto` changes nothing. Either value adds a start-up warning. Must be `auto` or `ask`. |
36-| `mmConfigs` | map name → path | none | Other mm agents this one can ask. Each name generates `/ask-<name>` and `/ask-with-context-<name>` (see [slash commands](slash-commands.md#asking-another-agent)). A relative path is relative to the directory of the file that declares it. Names: lowercase letters, digits and `-`, starting with a letter or a digit, not starting with `with-context`. A path whose file does not exist is a start-up warning, not an error. |
36+| `mmConfigs` | map name → path, or name → `{path, description, yolo}` | none | Other mm agents this one can ask. Each name generates `/ask-<name>` and `/ask-with-context-<name>` (see [slash commands](slash-commands.md#asking-another-agent)), and the list declares the [`ask_agent`](tools.md#ask_agent) tool, through which the model calls them itself. The long form adds `description` (when to call that agent; shown in `ask_agent`'s description, falling back to the agent file's `acp.title`) and `yolo` (decides that agent's tool calls when it is asked, by `/ask`, `ask_agent` or `mm --ask`, instead of this agent's `yolo`: `true` they run, `false` each one is asked; absent, this agent's `yolo` decides). `path` must not be empty. A relative path is relative to the directory of the file that declares it. Names: lowercase letters, digits and `-`, starting with a letter or a digit, not starting with `with-context`. A path whose file does not exist is a start-up warning, not an error. |
3737
3838 ## `context` keys
3939
@@ -124,7 +124,7 @@ Reported as `[config error: <path>: <message>]`, exit code 1.
124124 | `bashTimeout must be > 0` | zero or negative |
125125 | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` |
126126 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |
127-| `mmConfigs.<name>: the path must not be empty` | `<name>: ""` |
127+| `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` |
128128 | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. |
129129
130130 A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`).
@@ -33,7 +33,7 @@
33 | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. |33 | `tui` | bool | `false` | Start in the single-window mode without the `-tui` flag. Falls back to the line mode when stdin or stdout is not a terminal; ignored with `-acp`. |
34 | `yolo` | bool | `true` | `true`: the tools run without asking. `false`: every call that changes something — `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename`, `plan` `create`/`update`/`next`/`delete` — waits for the user's answer in every front end (line mode, window, editor), and so does every MCP tool, whatever `mcp.permissions` says. Answers: allow once, allow this tool for the session, reject this call, never (this tool, for the session). `read_file`, `read_skill`, `fetch_content` without `output` and the `lsp` navigation actions never ask. In `-p` and `mm --ask`, nobody can answer: those calls are rejected. An agent started by `/ask` runs with `AGENT_YOLO=false` and submits its calls to the asking agent, whose `yolo` decides. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). |34 | `yolo` | bool | `true` | `true`: the tools run without asking. `false`: every call that changes something — `bash`, `write_file`, `edit_file`, `fetch_content` with `output`, `lsp` `rename`, `plan` `create`/`update`/`next`/`delete` — waits for the user's answer in every front end (line mode, window, editor), and so does every MCP tool, whatever `mcp.permissions` says. Answers: allow once, allow this tool for the session, reject this call, never (this tool, for the session). `read_file`, `read_skill`, `fetch_content` without `output` and the `lsp` navigation actions never ask. In `-p` and `mm --ask`, nobody can answer: those calls are rejected. An agent started by `/ask` runs with `AGENT_YOLO=false` and submits its calls to the asking agent, whose `yolo` decides. Overridden by `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` still decide which tools exist. See [How to confirm tool calls](../how-to/confirm-tool-calls.md). |
35 | `permissions` | string | absent | Deprecated, replaced by `yolo`. `ask` = `yolo: false` (and wins over `yolo: true`); `auto` changes nothing. Either value adds a start-up warning. Must be `auto` or `ask`. |35 | `permissions` | string | absent | Deprecated, replaced by `yolo`. `ask` = `yolo: false` (and wins over `yolo: true`); `auto` changes nothing. Either value adds a start-up warning. Must be `auto` or `ask`. |
36-| `mmConfigs` | map name → path | none | Other mm agents this one can ask. Each name generates `/ask-<name>` and `/ask-with-context-<name>` (see [slash commands](slash-commands.md#asking-another-agent)). A relative path is relative to the directory of the file that declares it. Names: lowercase letters, digits and `-`, starting with a letter or a digit, not starting with `with-context`. A path whose file does not exist is a start-up warning, not an error. |36+| `mmConfigs` | map name → path, or name → `{path, description, yolo}` | none | Other mm agents this one can ask. Each name generates `/ask-<name>` and `/ask-with-context-<name>` (see [slash commands](slash-commands.md#asking-another-agent)), and the list declares the [`ask_agent`](tools.md#ask_agent) tool, through which the model calls them itself. The long form adds `description` (when to call that agent; shown in `ask_agent`'s description, falling back to the agent file's `acp.title`) and `yolo` (decides that agent's tool calls when it is asked, by `/ask`, `ask_agent` or `mm --ask`, instead of this agent's `yolo`: `true` they run, `false` each one is asked; absent, this agent's `yolo` decides). `path` must not be empty. A relative path is relative to the directory of the file that declares it. Names: lowercase letters, digits and `-`, starting with a letter or a digit, not starting with `with-context`. A path whose file does not exist is a start-up warning, not an error. |
37 37
38 ## `context` keys38 ## `context` keys
39 39
@@ -124,7 +124,7 @@ Reported as `[config error: <path>: <message>]`, exit code 1.
124 | `bashTimeout must be > 0` | zero or negative |124 | `bashTimeout must be > 0` | zero or negative |
125 | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` |125 | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` |
126 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |126 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | a name starting with `with-context` |
127-| `mmConfigs.<name>: the path must not be empty` | `<name>: ""` |127+| `mmConfigs.<name>: the path must not be empty` | `<name>: ""`, or a long form without `path` |
128 | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. |128 | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. |
129 129
130 A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`).130 A start-up warning, not an error, is printed when `context.enabled` is `true`, the window is unknown and `context.maxMessages` is `0`, and when the file sets the deprecated `permissions` key (`permissions: ask is deprecated — use yolo: false instead …`).
modified docs/en/reference/tools.md +22 -0
@@ -200,6 +200,28 @@ Declared only with `fetchContentTool: true`. Downloads a web page and returns it
200200 | `2` | `input` | Host unreachable, timeout, HTTP status ≥ 300 after redirections, more than 10 redirections, robots.txt forbids the page or cannot be read. |
201201 | `3` | `usage` | Empty or unparsable `url`, a scheme other than `http`/`https`, unknown `tables` value, `baseUrl` not absolute. |
202202
203+## `ask_agent`
204+
205+Declared when `mmConfigs` names at least one agent. Hands a task to one of them and returns its answer as the tool's result: the model decides when, from the agents' descriptions. Same machinery as `/ask` (package `internal/ask`): the agent runs as a child `mm -acp`, started on its first call and reused, with a fresh session per call.
206+
207+| Field | Type | Default | Description |
208+|-------|------|---------|-------------|
209+| `agent` | string | — | A name of `mmConfigs`. |
210+| `prompt` | string | — | The task, complete and self-contained. |
211+| `with_context` | bool | `false` | Send this conversation before the prompt: the history the turn started from (questions, answers, one line per tool call, never tool outputs), capped at `maxOutput` characters, newest kept. |
212+
213+| Aspect | Behaviour |
214+|--------|-----------|
215+| Description | A fixed paragraph, then `Agents:` and one ` <name> — <purpose>` line per agent, sorted by name. `<purpose>` is the entry's `description`, else the agent file's `acp.title` (unless it is the built-in one), else `(no description)`. |
216+| Permission | The call itself is gated like `bash`: requested when `yolo: false`. |
217+| The asked agent's calls | Decided by its entry's `yolo` in `mmConfigs` when set (`true` run, `false` asked), else by this agent's `yolo`. |
218+| Working directory | The process directory in the terminal and the window; the session `cwd` in ACP. |
219+| Display | The asked agent's text, thoughts and tool calls stream through the turn, its tool calls titled `<agent> · <title>`. The window names it while it works: header `mm [<acp.title>] · <model> @ <provider> · asked by <this agent>`, its answer under `‣ mm [<acp.title>]` (its `mmConfigs` name when it has no title). Line mode: `🗣 ask_agent: <agent> (<provider>/<model>) — <first line of the prompt>`, then the agent's output. |
220+| Watchdog | Each event of the asked agent (text, thought, tool call, answered permission) counts as a sign of life for this agent's watchdog. Silence longer than `watchdogTimeout` — a model loading, a long prompt being read — still ends the turn. |
221+| Result | The answer, truncated to `maxOutput`. The tool call's own output in the window and the editor is `<agent> answered (N characters)`: the answer has already streamed. Not added to the history as a `/ask` exchange: it is a tool result. |
222+| Errors | `[ask_agent: unknown agent "x" — agents: a, b]`; `[ask_agent: the prompt is empty — say what <agent> must do]`; `[ask_agent: <agent> failed: <reason>]`; `[ask_agent: <agent> gave no answer]`; a cancelled or broken run returns what was answered, then `[ask_agent: <agent> stopped before finishing: <reason>]`. |
223+| Depth | An asked agent may itself have `mmConfigs` and call `ask_agent`, up to two levels (`MM_ASK_DEPTH`); beyond, the call fails with the depth message. |
224+
203225 ## Loop detection
204226
205227 Every tool records `(tool, input, output)` in the loop detector after each call.
@@ -200,6 +200,28 @@ Declared only with `fetchContentTool: true`. Downloads a web page and returns it
200 | `2` | `input` | Host unreachable, timeout, HTTP status ≥ 300 after redirections, more than 10 redirections, robots.txt forbids the page or cannot be read. |200 | `2` | `input` | Host unreachable, timeout, HTTP status ≥ 300 after redirections, more than 10 redirections, robots.txt forbids the page or cannot be read. |
201 | `3` | `usage` | Empty or unparsable `url`, a scheme other than `http`/`https`, unknown `tables` value, `baseUrl` not absolute. |201 | `3` | `usage` | Empty or unparsable `url`, a scheme other than `http`/`https`, unknown `tables` value, `baseUrl` not absolute. |
202 202
203+## `ask_agent`
204+
205+Declared when `mmConfigs` names at least one agent. Hands a task to one of them and returns its answer as the tool's result: the model decides when, from the agents' descriptions. Same machinery as `/ask` (package `internal/ask`): the agent runs as a child `mm -acp`, started on its first call and reused, with a fresh session per call.
206+
207+| Field | Type | Default | Description |
208+|-------|------|---------|-------------|
209+| `agent` | string | — | A name of `mmConfigs`. |
210+| `prompt` | string | — | The task, complete and self-contained. |
211+| `with_context` | bool | `false` | Send this conversation before the prompt: the history the turn started from (questions, answers, one line per tool call, never tool outputs), capped at `maxOutput` characters, newest kept. |
212+
213+| Aspect | Behaviour |
214+|--------|-----------|
215+| Description | A fixed paragraph, then `Agents:` and one ` <name> — <purpose>` line per agent, sorted by name. `<purpose>` is the entry's `description`, else the agent file's `acp.title` (unless it is the built-in one), else `(no description)`. |
216+| Permission | The call itself is gated like `bash`: requested when `yolo: false`. |
217+| The asked agent's calls | Decided by its entry's `yolo` in `mmConfigs` when set (`true` run, `false` asked), else by this agent's `yolo`. |
218+| Working directory | The process directory in the terminal and the window; the session `cwd` in ACP. |
219+| Display | The asked agent's text, thoughts and tool calls stream through the turn, its tool calls titled `<agent> · <title>`. The window names it while it works: header `mm [<acp.title>] · <model> @ <provider> · asked by <this agent>`, its answer under `‣ mm [<acp.title>]` (its `mmConfigs` name when it has no title). Line mode: `🗣 ask_agent: <agent> (<provider>/<model>) — <first line of the prompt>`, then the agent's output. |
220+| Watchdog | Each event of the asked agent (text, thought, tool call, answered permission) counts as a sign of life for this agent's watchdog. Silence longer than `watchdogTimeout` — a model loading, a long prompt being read — still ends the turn. |
221+| Result | The answer, truncated to `maxOutput`. The tool call's own output in the window and the editor is `<agent> answered (N characters)`: the answer has already streamed. Not added to the history as a `/ask` exchange: it is a tool result. |
222+| Errors | `[ask_agent: unknown agent "x" — agents: a, b]`; `[ask_agent: the prompt is empty — say what <agent> must do]`; `[ask_agent: <agent> failed: <reason>]`; `[ask_agent: <agent> gave no answer]`; a cancelled or broken run returns what was answered, then `[ask_agent: <agent> stopped before finishing: <reason>]`. |
223+| Depth | An asked agent may itself have `mmConfigs` and call `ask_agent`, up to two levels (`MM_ASK_DEPTH`); beyond, the call fails with the depth message. |
224+
203 ## Loop detection225 ## Loop detection
204 226
205 Every tool records `(tool, input, output)` in the loop detector after each call.227 Every tool records `(tool, input, output)` in the loop detector after each call.
modified docs/fr/README.md +3 -2
@@ -14,7 +14,8 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https:
1414 - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demo-llmman/`
1515 - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demo-ollama/`
1616 - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>`
17-- [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, les skills de démo
17+- [Laisser votre agent appeler d'autres agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, descriptions dans `mmConfigs`, `yolo` par agent
18+- [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`
1819 - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md)
1920 - [Couper le raisonnement d'un modèle](how-to/turn-reasoning-off.md) — `chat_template_kwargs` ou `reasoning_effort` dans `sampling`, selon le serveur
2021 - [Confirmer les appels d'outils avant leur exécution](how-to/confirm-tool-calls.md) — `yolo: false`, les quatre réponses, `AGENT_YOLO`, qui décide avec `mmConfigs`
@@ -61,7 +62,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https:
6162 - [L'outil `fetch_content`](explanation/fetch-content-tool.md) — pourquoi un outil intégré à côté du `fetch` MCP, un convertisseur maison, le robots.txt, des erreurs numérotées en texte, les limites connues
6263 - [Modèles à raisonnement](explanation/thinking-models.md) — d'où vient le raisonnement d'un modèle, pourquoi `mm` le lit sous Genkit, et pourquoi le lire garde le chien de garde en vie
6364 - [Le mode yolo et les permissions](explanation/yolo-and-permissions.md) — pourquoi les outils s'exécutent sans demander par défaut, exister ou demander, quatre réponses dans un seul paquet, pourquoi l'agent qui interroge décide
64-- [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, la limite de profondeur
65+- [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, `ask_agent` et le `yolo` par agent, la limite de profondeur
6566 - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio)
6667
6768 ### Explication MCP (Model Context Protocol)
@@ -14,7 +14,8 @@ 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émo17+- [Laisser votre agent appeler d'autres agents](how-to/let-the-agent-call-other-agents.md) — `ask_agent`, descriptions dans `mmConfigs`, `yolo` par agent
18+- [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`
18 - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md)19 - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md)
19 - [Couper le raisonnement d'un modèle](how-to/turn-reasoning-off.md) — `chat_template_kwargs` ou `reasoning_effort` dans `sampling`, selon le serveur20 - [Couper le raisonnement d'un modèle](how-to/turn-reasoning-off.md) — `chat_template_kwargs` ou `reasoning_effort` dans `sampling`, selon le serveur
20 - [Confirmer les appels d'outils avant leur exécution](how-to/confirm-tool-calls.md) — `yolo: false`, les quatre réponses, `AGENT_YOLO`, qui décide avec `mmConfigs`21 - [Confirmer les appels d'outils avant leur exécution](how-to/confirm-tool-calls.md) — `yolo: false`, les quatre réponses, `AGENT_YOLO`, qui décide avec `mmConfigs`
@@ -61,7 +62,7 @@ Cette documentation est organisée selon les quatre quadrants [Diátaxis](https:
61 - [L'outil `fetch_content`](explanation/fetch-content-tool.md) — pourquoi un outil intégré à côté du `fetch` MCP, un convertisseur maison, le robots.txt, des erreurs numérotées en texte, les limites connues62 - [L'outil `fetch_content`](explanation/fetch-content-tool.md) — pourquoi un outil intégré à côté du `fetch` MCP, un convertisseur maison, le robots.txt, des erreurs numérotées en texte, les limites connues
62 - [Modèles à raisonnement](explanation/thinking-models.md) — d'où vient le raisonnement d'un modèle, pourquoi `mm` le lit sous Genkit, et pourquoi le lire garde le chien de garde en vie63 - [Modèles à raisonnement](explanation/thinking-models.md) — d'où vient le raisonnement d'un modèle, pourquoi `mm` le lit sous Genkit, et pourquoi le lire garde le chien de garde en vie
63 - [Le mode yolo et les permissions](explanation/yolo-and-permissions.md) — pourquoi les outils s'exécutent sans demander par défaut, exister ou demander, quatre réponses dans un seul paquet, pourquoi l'agent qui interroge décide64 - [Le mode yolo et les permissions](explanation/yolo-and-permissions.md) — pourquoi les outils s'exécutent sans demander par défaut, exister ou demander, quatre réponses dans un seul paquet, pourquoi l'agent qui interroge décide
64-- [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, la limite de profondeur65+- [Interroger un autre agent](explanation/asking-another-agent.md) — pourquoi un autre agent mm est un processus enfant interrogé en ACP, la paire de messages, les sessions neuves, `ask_agent` et le `yolo` par agent, la limite de profondeur
65 - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio)66 - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio)
66 67
67 ### Explication MCP (Model Context Protocol)68 ### Explication MCP (Model Context Protocol)
modified docs/fr/explanation/asking-another-agent.md +7 -4
@@ -2,7 +2,7 @@
22
33 ## De quoi s'agit-il ?
44
5-`/ask-bob <prompt>` permet à l'agent avec qui vous parlez de consulter un autre agent mm : un autre modèle, un autre prompt système, d'autres outils et skills. La réponse de bob est affichée, puis gardée dans votre conversation pour que votre propre modèle s'en serve à votre question suivante. Cette page explique comment c'est construit et pourquoi.
5+`/ask-bob <prompt>` permet à l'agent avec qui vous parlez de consulter un autre agent mm : un autre modèle, un autre prompt système, d'autres outils et skills. La réponse de bob est affichée, puis gardée dans votre conversation pour que votre propre modèle s'en serve à votre question suivante. L'outil `ask_agent` permet à votre modèle de faire de même de lui-même, quand une demande correspond au rôle d'un agent. Cette page explique comment c'est construit et pourquoi.
66
77 ## Pourquoi c'est conçu ainsi
88
@@ -10,7 +10,7 @@
1010
1111 **Les deux parlent ACP.** mm parle déjà l'Agent Client Protocol comme serveur, pour les éditeurs. Interroger bob fait de l'agent qui pose la question un *client* ACP de son enfant. Le protocole apporte gratuitement ce que lire un terminal n'aurait jamais donné : la réponse de bob sous forme de texte, ses appels d'outils comme des événements structurés, ses demandes de permission comme des requêtes auxquelles on peut répondre, et une façon standard d'annuler. Le serveur ACP de mm n'appelle que deux méthodes du client — les mises à jour de session et les demandes de permission — donc le côté client est petit ; les requêtes de fichiers et de terminal sont refusées comme absentes.
1212
13-**Les permissions de bob, c'est vous qui les donnez.** Bob est lancé avec `AGENT_YOLO=false` : chaque appel qu'il filtrerait devient une demande de permission adressée à votre agent, et c'est VOTRE `yolo` qui décide : activé, l'appel s'exécute ; désactivé, la question va à la façade que vous utilisez — le dialogue de l'éditeur, la zone de la fenêtre, l'invite `🔐` du mode ligne — exactement comme pour les outils de votre propre agent à cet endroit. Le `yolo` du fichier de bob ne compte pas tant qu'il vous répond. Les outils de bob s'appellent `bob/<outil>`, donc autoriser le `bash` de votre agent « pour cette session » n'autorise pas celui de bob. Voir [Le mode yolo et les permissions](yolo-and-permissions.md).
13+**Les permissions de bob, c'est vous qui les donnez.** Bob est lancé avec `AGENT_YOLO=false` : chaque appel qu'il filtrerait devient une demande de permission adressée à votre agent, et c'est VOTRE `yolo` qui décide : activé, l'appel s'exécute ; désactivé, la question va à la façade que vous utilisez — le dialogue de l'éditeur, la zone de la fenêtre, l'invite `🔐` du mode ligne — exactement comme pour les outils de votre propre agent à cet endroit. Le `yolo` du fichier de bob ne compte pas tant qu'il vous répond — celui de l'entrée qui le décrit dans VOTRE fichier, si : `mmConfigs: {bob: {path: …, yolo: true}}` laisse s'exécuter les appels de bob alors que les vôtres demandent toujours (un codeur à qui l'on confie les modifications une fois le passage de main approuvé), `yolo: false` demande pour chacun de ses appels même quand les vôtres s'exécutent librement. Les outils de bob s'appellent `bob/<outil>`, donc autoriser le `bash` de votre agent « pour cette session » n'autorise pas celui de bob. Voir [Le mode yolo et les permissions](yolo-and-permissions.md).
1414
1515 **La réponse est une paire de messages.** Elle entre dans votre historique comme un message utilisateur (`[asked bob (fournisseur/modèle): …]`) suivi d'un message de l'assistant (`[bob's answer]` et le texte). Un seul message aurait été plus simple, mais votre question suivante est aussi un message utilisateur, et plusieurs templates de chat (ceux de Gemma, de Mistral) refusent deux messages utilisateur à la suite. Les marques disent qui a dit quoi, pour que votre modèle ne prenne pas la réponse de bob pour la sienne. La réponse est plafonnée comme la sortie d'un outil, à `maxOutput` caractères : elle atterrit dans la fenêtre de votre modèle.
1616
@@ -20,11 +20,13 @@
2020
2121 **L'enfant est protégé de ce qui est destiné à son parent.** Des variables comme `AGENT_MODEL`, `AGENT_PROVIDER` ou `AGENT_BASE_URL` surchargent une configuration ; héritées par bob, elles en feraient silencieusement une copie de vous. Elles sont retirées de son environnement. Bob tourne aussi avec `AGENT_SESSIONS=off` : il travaille dans votre projet, et ses fichiers de session apparaîtraient sinon dans vos `/sessions` et `/resume`.
2222
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+**Votre modèle peut aussi décider : `ask_agent`.** Dès que `mmConfigs` est renseigné, votre agent reçoit un outil intégré qui interroge l'un des agents et renvoie sa réponse comme résultat de l'outil. Sa description liste les agents avec le rôle de chacun — la `description` de l'entrée, à défaut l'`acp.title` de l'agent — et c'est tout ce dont le modèle dispose pour choisir : une description dit donc quand appeler l'agent, pas qui il est. L'outil s'appuie sur le même pool et le même protocole que `/ask`, à l'intérieur du tour : le texte, les pensées et les appels d'outils de l'agent défilent dans votre façade au fur et à mesure, et la fenêtre le nomme pendant qu'il travaille — son titre et son modèle dans l'en-tête, `asked by` votre agent, sa réponse étiquetée à son nom. L'appel lui-même est soumis comme `bash` : avec `yolo: false`, vous voyez le prompt écrit par votre modèle avant qu'il parte. La réponse est un résultat d'outil, pas une paire `/ask` dans l'historique : votre modèle la lit et poursuit dans le même tour — la rapporter, la vérifier, confier l'étape suivante à un autre agent.
24+
25+**Les chaînes s'arrêtent.** Un modèle qui appelle `ask_agent` depuis un agent lui-même interrogé pourrait continuer à interroger ; chaque enfant connaît donc sa profondeur (`MM_ASK_DEPTH`) : votre agent peut interroger bob, bob peut interroger riker, riker ne peut interroger personne — que ce soit par un `/ask` tapé ou par `ask_agent`.
2426
2527 **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 suivent le `yolo` de la configuration appelante : autorisés quand il est activé — la personne a déjà validé la commande `mm --ask` elle-même —, refusés quand il est désactivé, puisque stdin portait la question et que personne n'est là pour répondre. 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.
2628
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é.
29+**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é. `ask_agent` fait mieux parce qu'il voit l'agent travailler : chaque événement que l'agent diffuse — du texte, une pensée, un appel d'outil — compte comme un signe de vie pour le watchdog de votre agent. Seul un long silence termine encore le tour : un agent qui charge un gros modèle, ou qui lit un long premier prompt ; `watchdogTimeout` doit couvrir cette durée.
2830
2931 ## Alternatives écartées
3032
@@ -32,6 +34,7 @@
3234 - **Un second moteur dans le même processus.** Rapide, sans processus — au prix de transformer la configuration globale en configuration par agent dans tout le code.
3335 - **Faire répondre le modèle courant tout de suite avec l'apport de bob.** L'utilisateur a choisi autrement : la réponse de bob est affichée et gardée, et vous décidez de la question suivante.
3436 - **Garder la conversation de bob d'une demande à l'autre.** Deux conversations qui évoluent en parallèle, dont une invisible, sont plus difficiles à suivre qu'une transcription explicite envoyée quand on la demande.
37+- **Des skills qui lancent `mm --ask` pour le modèle.** Ça marche — une skill par passage de main, la commande dans son texte — et `mm --ask` reste pour les scripts. Mais le travail de l'agent vous parvient comme la sortie d'une commande `bash` : pas de nom dans l'en-tête, pas d'étiquette sur sa réponse, ses appels d'outils en lignes de texte. Et un petit coordinateur doit recopier une longue commande à l'identique : mesuré avec Gemma 4 E4B, il a oublié les guillemets et un préfixe `AGENT_YOLO=true`, si bien que le codeur appelé ne pouvait pas écrire. Un appel d'outil avec un `agent` et un `prompt` n'a rien à recopier.
3538 - **Seulement un `/ask <nom>` générique, y compris dans les éditeurs.** Le menu `/` d'un éditeur complète un nom de commande, jamais son argument ; une commande par agent est la seule façon pour que le menu liste les agents.
3639
3740 ## Lien avec le reste
@@ -2,7 +2,7 @@
2 2
3 ## De quoi s'agit-il ?3 ## De quoi s'agit-il ?
4 4
5-`/ask-bob <prompt>` permet à l'agent avec qui vous parlez de consulter un autre agent mm : un autre modèle, un autre prompt système, d'autres outils et skills. La réponse de bob est affichée, puis gardée dans votre conversation pour que votre propre modèle s'en serve à votre question suivante. Cette page explique comment c'est construit et pourquoi.5+`/ask-bob <prompt>` permet à l'agent avec qui vous parlez de consulter un autre agent mm : un autre modèle, un autre prompt système, d'autres outils et skills. La réponse de bob est affichée, puis gardée dans votre conversation pour que votre propre modèle s'en serve à votre question suivante. L'outil `ask_agent` permet à votre modèle de faire de même de lui-même, quand une demande correspond au rôle d'un agent. Cette page explique comment c'est construit et pourquoi.
6 6
7 ## Pourquoi c'est conçu ainsi7 ## Pourquoi c'est conçu ainsi
8 8
@@ -10,7 +10,7 @@
10 10
11 **Les deux parlent ACP.** mm parle déjà l'Agent Client Protocol comme serveur, pour les éditeurs. Interroger bob fait de l'agent qui pose la question un *client* ACP de son enfant. Le protocole apporte gratuitement ce que lire un terminal n'aurait jamais donné : la réponse de bob sous forme de texte, ses appels d'outils comme des événements structurés, ses demandes de permission comme des requêtes auxquelles on peut répondre, et une façon standard d'annuler. Le serveur ACP de mm n'appelle que deux méthodes du client — les mises à jour de session et les demandes de permission — donc le côté client est petit ; les requêtes de fichiers et de terminal sont refusées comme absentes.11 **Les deux parlent ACP.** mm parle déjà l'Agent Client Protocol comme serveur, pour les éditeurs. Interroger bob fait de l'agent qui pose la question un *client* ACP de son enfant. Le protocole apporte gratuitement ce que lire un terminal n'aurait jamais donné : la réponse de bob sous forme de texte, ses appels d'outils comme des événements structurés, ses demandes de permission comme des requêtes auxquelles on peut répondre, et une façon standard d'annuler. Le serveur ACP de mm n'appelle que deux méthodes du client — les mises à jour de session et les demandes de permission — donc le côté client est petit ; les requêtes de fichiers et de terminal sont refusées comme absentes.
12 12
13-**Les permissions de bob, c'est vous qui les donnez.** Bob est lancé avec `AGENT_YOLO=false` : chaque appel qu'il filtrerait devient une demande de permission adressée à votre agent, et c'est VOTRE `yolo` qui décide : activé, l'appel s'exécute ; désactivé, la question va à la façade que vous utilisez — le dialogue de l'éditeur, la zone de la fenêtre, l'invite `🔐` du mode ligne — exactement comme pour les outils de votre propre agent à cet endroit. Le `yolo` du fichier de bob ne compte pas tant qu'il vous répond. Les outils de bob s'appellent `bob/<outil>`, donc autoriser le `bash` de votre agent « pour cette session » n'autorise pas celui de bob. Voir [Le mode yolo et les permissions](yolo-and-permissions.md).13+**Les permissions de bob, c'est vous qui les donnez.** Bob est lancé avec `AGENT_YOLO=false` : chaque appel qu'il filtrerait devient une demande de permission adressée à votre agent, et c'est VOTRE `yolo` qui décide : activé, l'appel s'exécute ; désactivé, la question va à la façade que vous utilisez — le dialogue de l'éditeur, la zone de la fenêtre, l'invite `🔐` du mode ligne — exactement comme pour les outils de votre propre agent à cet endroit. Le `yolo` du fichier de bob ne compte pas tant qu'il vous répond — celui de l'entrée qui le décrit dans VOTRE fichier, si : `mmConfigs: {bob: {path: …, yolo: true}}` laisse s'exécuter les appels de bob alors que les vôtres demandent toujours (un codeur à qui l'on confie les modifications une fois le passage de main approuvé), `yolo: false` demande pour chacun de ses appels même quand les vôtres s'exécutent librement. Les outils de bob s'appellent `bob/<outil>`, donc autoriser le `bash` de votre agent « pour cette session » n'autorise pas celui de bob. Voir [Le mode yolo et les permissions](yolo-and-permissions.md).
14 14
15 **La réponse est une paire de messages.** Elle entre dans votre historique comme un message utilisateur (`[asked bob (fournisseur/modèle): …]`) suivi d'un message de l'assistant (`[bob's answer]` et le texte). Un seul message aurait été plus simple, mais votre question suivante est aussi un message utilisateur, et plusieurs templates de chat (ceux de Gemma, de Mistral) refusent deux messages utilisateur à la suite. Les marques disent qui a dit quoi, pour que votre modèle ne prenne pas la réponse de bob pour la sienne. La réponse est plafonnée comme la sortie d'un outil, à `maxOutput` caractères : elle atterrit dans la fenêtre de votre modèle.15 **La réponse est une paire de messages.** Elle entre dans votre historique comme un message utilisateur (`[asked bob (fournisseur/modèle): …]`) suivi d'un message de l'assistant (`[bob's answer]` et le texte). Un seul message aurait été plus simple, mais votre question suivante est aussi un message utilisateur, et plusieurs templates de chat (ceux de Gemma, de Mistral) refusent deux messages utilisateur à la suite. Les marques disent qui a dit quoi, pour que votre modèle ne prenne pas la réponse de bob pour la sienne. La réponse est plafonnée comme la sortie d'un outil, à `maxOutput` caractères : elle atterrit dans la fenêtre de votre modèle.
16 16
@@ -20,11 +20,13 @@
20 20
21 **L'enfant est protégé de ce qui est destiné à son parent.** Des variables comme `AGENT_MODEL`, `AGENT_PROVIDER` ou `AGENT_BASE_URL` surchargent une configuration ; héritées par bob, elles en feraient silencieusement une copie de vous. Elles sont retirées de son environnement. Bob tourne aussi avec `AGENT_SESSIONS=off` : il travaille dans votre projet, et ses fichiers de session apparaîtraient sinon dans vos `/sessions` et `/resume`.21 **L'enfant est protégé de ce qui est destiné à son parent.** Des variables comme `AGENT_MODEL`, `AGENT_PROVIDER` ou `AGENT_BASE_URL` surchargent une configuration ; héritées par bob, elles en feraient silencieusement une copie de vous. Elles sont retirées de son environnement. Bob tourne aussi avec `AGENT_SESSIONS=off` : il travaille dans votre projet, et ses fichiers de session apparaîtraient sinon dans vos `/sessions` et `/resume`.
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+**Votre modèle peut aussi décider : `ask_agent`.** Dès que `mmConfigs` est renseigné, votre agent reçoit un outil intégré qui interroge l'un des agents et renvoie sa réponse comme résultat de l'outil. Sa description liste les agents avec le rôle de chacun — la `description` de l'entrée, à défaut l'`acp.title` de l'agent — et c'est tout ce dont le modèle dispose pour choisir : une description dit donc quand appeler l'agent, pas qui il est. L'outil s'appuie sur le même pool et le même protocole que `/ask`, à l'intérieur du tour : le texte, les pensées et les appels d'outils de l'agent défilent dans votre façade au fur et à mesure, et la fenêtre le nomme pendant qu'il travaille — son titre et son modèle dans l'en-tête, `asked by` votre agent, sa réponse étiquetée à son nom. L'appel lui-même est soumis comme `bash` : avec `yolo: false`, vous voyez le prompt écrit par votre modèle avant qu'il parte. La réponse est un résultat d'outil, pas une paire `/ask` dans l'historique : votre modèle la lit et poursuit dans le même tour — la rapporter, la vérifier, confier l'étape suivante à un autre agent.
24+
25+**Les chaînes s'arrêtent.** Un modèle qui appelle `ask_agent` depuis un agent lui-même interrogé pourrait continuer à interroger ; chaque enfant connaît donc sa profondeur (`MM_ASK_DEPTH`) : votre agent peut interroger bob, bob peut interroger riker, riker ne peut interroger personne — que ce soit par un `/ask` tapé ou par `ask_agent`.
24 26
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 suivent le `yolo` de la configuration appelante : autorisés quand il est activé — la personne a déjà validé la commande `mm --ask` elle-même —, refusés quand il est désactivé, puisque stdin portait la question et que personne n'est là pour répondre. 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.27 **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 suivent le `yolo` de la configuration appelante : autorisés quand il est activé — la personne a déjà validé la commande `mm --ask` elle-même —, refusés quand il est désactivé, puisque stdin portait la question et que personne n'est là pour répondre. 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 28
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é.29+**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é. `ask_agent` fait mieux parce qu'il voit l'agent travailler : chaque événement que l'agent diffuse — du texte, une pensée, un appel d'outil — compte comme un signe de vie pour le watchdog de votre agent. Seul un long silence termine encore le tour : un agent qui charge un gros modèle, ou qui lit un long premier prompt ; `watchdogTimeout` doit couvrir cette durée.
28 30
29 ## Alternatives écartées31 ## Alternatives écartées
30 32
@@ -32,6 +34,7 @@
32 - **Un second moteur dans le même processus.** Rapide, sans processus — au prix de transformer la configuration globale en configuration par agent dans tout le code.34 - **Un second moteur dans le même processus.** Rapide, sans processus — au prix de transformer la configuration globale en configuration par agent dans tout le code.
33 - **Faire répondre le modèle courant tout de suite avec l'apport de bob.** L'utilisateur a choisi autrement : la réponse de bob est affichée et gardée, et vous décidez de la question suivante.35 - **Faire répondre le modèle courant tout de suite avec l'apport de bob.** L'utilisateur a choisi autrement : la réponse de bob est affichée et gardée, et vous décidez de la question suivante.
34 - **Garder la conversation de bob d'une demande à l'autre.** Deux conversations qui évoluent en parallèle, dont une invisible, sont plus difficiles à suivre qu'une transcription explicite envoyée quand on la demande.36 - **Garder la conversation de bob d'une demande à l'autre.** Deux conversations qui évoluent en parallèle, dont une invisible, sont plus difficiles à suivre qu'une transcription explicite envoyée quand on la demande.
37+- **Des skills qui lancent `mm --ask` pour le modèle.** Ça marche — une skill par passage de main, la commande dans son texte — et `mm --ask` reste pour les scripts. Mais le travail de l'agent vous parvient comme la sortie d'une commande `bash` : pas de nom dans l'en-tête, pas d'étiquette sur sa réponse, ses appels d'outils en lignes de texte. Et un petit coordinateur doit recopier une longue commande à l'identique : mesuré avec Gemma 4 E4B, il a oublié les guillemets et un préfixe `AGENT_YOLO=true`, si bien que le codeur appelé ne pouvait pas écrire. Un appel d'outil avec un `agent` et un `prompt` n'a rien à recopier.
35 - **Seulement un `/ask <nom>` générique, y compris dans les éditeurs.** Le menu `/` d'un éditeur complète un nom de commande, jamais son argument ; une commande par agent est la seule façon pour que le menu liste les agents.38 - **Seulement un `/ask <nom>` générique, y compris dans les éditeurs.** Le menu `/` d'un éditeur complète un nom de commande, jamais son argument ; une commande par agent est la seule façon pour que le menu liste les agents.
36 39
37 ## Lien avec le reste40 ## Lien avec le reste
modified docs/fr/how-to/ask-another-agent-from-a-skill.md +24 -5
@@ -1,6 +1,6 @@
11 # Comment interroger un autre agent depuis une skill ou un script
22
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)).
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)). Pour que votre modèle confie du travail à un autre agent de lui-même, l'[outil `ask_agent`](let-the-agent-call-other-agents.md) est la voie la plus simple : aucune commande à recopier, et la fenêtre montre quel agent travaille. Une skill sert pour une procédure figée, un script, ou plusieurs agents en une seule commande.
44
55 ## Étapes
66
@@ -25,11 +25,30 @@ Ce guide montre comment faire poser une question à un autre agent mm par une sk
2525
2626 C'est le modèle qui écrit ce résumé lui-même : une commande ne peut pas lire la conversation.
2727
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 :
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. Une skill complète (`skills/second-opinion/SKILL.md`) :
2929
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.
30+ ```markdown
31+ ---
32+ name: second-opinion
33+ description: Get a second opinion from the reviewer agent on a claim or an answer from this conversation.
34+ keywords: second opinion, review, check, deuxième avis, vérifie, relis
35+ ---
36+
37+ # Second opinion
38+
39+ Call your `bash` tool ONCE with this command, the conversation summarised in English above the `---` line, the question below it:
40+
41+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
42+ User: <the claim>
43+ Assistant: <your answer, if any>
44+ ---
45+ Is this right? Correct it if not.
46+ CONTEXT
47+
48+ What it prints is the reviewer's answer. Report it, starting with "The reviewer says:"; never answer in its place. If it prints `[ask: …]`, tell the user that line.
49+ ```
50+
51+ Gardez une telle commande courte : un petit modèle recopie une longue commande de façon inexacte (guillemets, préfixes).
3352
3453 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 :
3554
@@ -1,6 +1,6 @@
1 # Comment interroger un autre agent depuis une skill ou un script1 # Comment interroger un autre agent depuis une skill ou un script
2 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)).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)). Pour que votre modèle confie du travail à un autre agent de lui-même, l'[outil `ask_agent`](let-the-agent-call-other-agents.md) est la voie la plus simple : aucune commande à recopier, et la fenêtre montre quel agent travaille. Une skill sert pour une procédure figée, un script, ou plusieurs agents en une seule commande.
4 4
5 ## Étapes5 ## Étapes
6 6
@@ -25,11 +25,30 @@ Ce guide montre comment faire poser une question à un autre agent mm par une sk
25 25
26 C'est le modèle qui écrit ce résumé lui-même : une commande ne peut pas lire la conversation.26 C'est le modèle qui écrit ce résumé lui-même : une commande ne peut pas lire la conversation.
27 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 :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. Une skill complète (`skills/second-opinion/SKILL.md`) :
29 29
30- - `delegate-to-jan` — une question déléguée à Jan ;30+ ```markdown
31- - `second-opinion` — la conversation envoyée à un relecteur ;31+ ---
32- - `compare-agents` — la même question à Jan et à Lucy, en une seule commande.32+ name: second-opinion
33+ description: Get a second opinion from the reviewer agent on a claim or an answer from this conversation.
34+ keywords: second opinion, review, check, deuxième avis, vérifie, relis
35+ ---
36+
37+ # Second opinion
38+
39+ Call your `bash` tool ONCE with this command, the conversation summarised in English above the `---` line, the question below it:
40+
41+ "$MM_BIN" --ask-with-context reviewer <<'CONTEXT'
42+ User: <the claim>
43+ Assistant: <your answer, if any>
44+ ---
45+ Is this right? Correct it if not.
46+ CONTEXT
47+
48+ What it prints is the reviewer's answer. Report it, starting with "The reviewer says:"; never answer in its place. If it prints `[ask: …]`, tell the user that line.
49+ ```
50+
51+ Gardez une telle commande courte : un petit modèle recopie une longue commande de façon inexacte (guillemets, préfixes).
33 52
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 :53 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 54
added docs/fr/how-to/let-the-agent-call-other-agents.md +73 -0
new file mode 100644
@@ -0,0 +1,73 @@
1+# Comment laisser votre agent appeler d'autres agents
2+
3+Ce guide montre comment donner une équipe à votre agent : d'autres agents mm auxquels il confie du travail de lui-même, avec l'outil intégré `ask_agent`, pendant que vous voyez lequel travaille. Il suppose que vous lancez déjà `mm` avec un fichier de configuration, et que vous avez un fichier de configuration par agent à appeler.
4+
5+## Étapes
6+
7+1. Listez les agents sous `mmConfigs`, chacun avec une `description` qui dit **quand** l'appeler — la description est tout ce dont votre modèle dispose pour choisir :
8+
9+ ```yaml
10+ mmConfigs:
11+ milo:
12+ path: agent.milo.yaml
13+ description: Specifier. Turns a request into a specification saved in specs/<slug>.spec.md, and checks code against a specification.
14+ homer:
15+ path: agent.homer.yaml
16+ description: Coder. Implements a specification file from specs/, with tests. Call him only once a specification exists.
17+ ```
18+
19+ Les chemins sont relatifs au fichier où vous les écrivez. Une entrée simple `nom: chemin` marche toujours ; elle est décrite par l'`acp.title` de l'agent.
20+
21+2. Décidez qui approuve les appels d'outils des agents eux-mêmes. Par défaut, le `yolo` de votre agent décide aussi pour eux. Donnez à un agent son propre `yolo` pour changer cela :
22+
23+ ```yaml
24+ homer:
25+ path: agent.homer.yaml
26+ description: …
27+ yolo: true # ses modifications et commandes s'exécutent sans demander
28+ ```
29+
30+ Avec votre propre `yolo: false`, vous approuvez toujours chaque passage de main — l'appel `ask_agent`, avec le prompt écrit par votre modèle — puis homer travaille seul.
31+
32+3. Dites à votre modèle, dans son prompt `system`, quand passer la main et quoi faire de la réponse. Par exemple :
33+
34+ ```yaml
35+ system: |
36+ …
37+ OTHER AGENTS
38+ `ask_agent` hands a task to another agent; its description says which
39+ agent does what. Give it a complete prompt. Call it ONCE per task: its
40+ result is that agent's answer. Then answer the user, saying which agent
41+ answered; never answer in its place.
42+ ```
43+
44+4. Lancez `mm` et vérifiez que la bannière liste `ask_agent` parmi les outils :
45+
46+ ```text
47+ mm … | tools: bash, read_skill, read_file, write_file, edit_file, plan, ask_agent | …
48+ ```
49+
50+5. Demandez en langage courant quelque chose qui relève d'un agent. Dans la fenêtre (`mm -tui`), l'en-tête bascule sur l'agent pendant qu'il travaille, et sa réponse porte son nom :
51+
52+ ```text
53+ mm [Jan (coding agent)] · Jan-nano:Q4_K_M @ llmman · asked by mm [Bob (coordinator)]
54+ …
55+ ‣ mm [Jan (coding agent)]
56+ The capital of Peru is Lima.
57+ ```
58+
59+ Puis l'en-tête revient à votre agent, qui poursuit à partir de la réponse.
60+
61+## Variantes
62+
63+- **Envoyer la conversation.** Le modèle met `with_context: true` quand l'agent a besoin de ce qui a été dit — un relecteur qui donne un deuxième avis. Dites-le dans la description de l'agent (« call it with with_context true »).
64+- **Interroger un agent vous-même.** `/ask-<nom>` et `/ask-with-context-<nom>` marchent toujours, et gardent l'échange dans l'historique. Voir [Interroger un autre agent mm](ask-another-agent.md).
65+- **Travail long.** Chaque événement diffusé par l'agent maintient en vie le watchdog de votre agent, mais pas un long silence — un gros modèle qui se charge, un long premier prompt : relevez votre `watchdogTimeout` pour le couvrir (10m pour un modèle de 48 Go sur un portable).
66+- **Depuis un script.** `mm --ask <nom>` reste la voie pour une commande shell. Voir [Interroger un autre agent depuis une skill ou un script](ask-another-agent-from-a-skill.md).
67+- **Exemples complets.** `demo-llmman/agent.llmman.yaml` (Bob avec Jan, Lucy et un relecteur) et `mini-mes/riker/agent.riker.yaml` (un coordinateur, un spécificateur et un codeur).
68+
69+## Voir aussi
70+
71+- Les champs de l'outil, son affichage et ses erreurs : [référence des outils](../reference/tools.md#ask_agent)
72+- Les deux formes de `mmConfigs` : [référence de la configuration](../reference/configuration.md)
73+- Pourquoi il tourne dans le tour plutôt que par une skill : [interroger un autre agent — explication](../explanation/asking-another-agent.md)
new file mode 100644
@@ -0,0 +1,73 @@
1+# Comment laisser votre agent appeler d'autres agents
2+
3+Ce guide montre comment donner une équipe à votre agent : d'autres agents mm auxquels il confie du travail de lui-même, avec l'outil intégré `ask_agent`, pendant que vous voyez lequel travaille. Il suppose que vous lancez déjà `mm` avec un fichier de configuration, et que vous avez un fichier de configuration par agent à appeler.
4+
5+## Étapes
6+
7+1. Listez les agents sous `mmConfigs`, chacun avec une `description` qui dit **quand** l'appeler — la description est tout ce dont votre modèle dispose pour choisir :
8+
9+ ```yaml
10+ mmConfigs:
11+ milo:
12+ path: agent.milo.yaml
13+ description: Specifier. Turns a request into a specification saved in specs/<slug>.spec.md, and checks code against a specification.
14+ homer:
15+ path: agent.homer.yaml
16+ description: Coder. Implements a specification file from specs/, with tests. Call him only once a specification exists.
17+ ```
18+
19+ Les chemins sont relatifs au fichier où vous les écrivez. Une entrée simple `nom: chemin` marche toujours ; elle est décrite par l'`acp.title` de l'agent.
20+
21+2. Décidez qui approuve les appels d'outils des agents eux-mêmes. Par défaut, le `yolo` de votre agent décide aussi pour eux. Donnez à un agent son propre `yolo` pour changer cela :
22+
23+ ```yaml
24+ homer:
25+ path: agent.homer.yaml
26+ description: …
27+ yolo: true # ses modifications et commandes s'exécutent sans demander
28+ ```
29+
30+ Avec votre propre `yolo: false`, vous approuvez toujours chaque passage de main — l'appel `ask_agent`, avec le prompt écrit par votre modèle — puis homer travaille seul.
31+
32+3. Dites à votre modèle, dans son prompt `system`, quand passer la main et quoi faire de la réponse. Par exemple :
33+
34+ ```yaml
35+ system: |
36+ …
37+ OTHER AGENTS
38+ `ask_agent` hands a task to another agent; its description says which
39+ agent does what. Give it a complete prompt. Call it ONCE per task: its
40+ result is that agent's answer. Then answer the user, saying which agent
41+ answered; never answer in its place.
42+ ```
43+
44+4. Lancez `mm` et vérifiez que la bannière liste `ask_agent` parmi les outils :
45+
46+ ```text
47+ mm … | tools: bash, read_skill, read_file, write_file, edit_file, plan, ask_agent | …
48+ ```
49+
50+5. Demandez en langage courant quelque chose qui relève d'un agent. Dans la fenêtre (`mm -tui`), l'en-tête bascule sur l'agent pendant qu'il travaille, et sa réponse porte son nom :
51+
52+ ```text
53+ mm [Jan (coding agent)] · Jan-nano:Q4_K_M @ llmman · asked by mm [Bob (coordinator)]
54+ …
55+ ‣ mm [Jan (coding agent)]
56+ The capital of Peru is Lima.
57+ ```
58+
59+ Puis l'en-tête revient à votre agent, qui poursuit à partir de la réponse.
60+
61+## Variantes
62+
63+- **Envoyer la conversation.** Le modèle met `with_context: true` quand l'agent a besoin de ce qui a été dit — un relecteur qui donne un deuxième avis. Dites-le dans la description de l'agent (« call it with with_context true »).
64+- **Interroger un agent vous-même.** `/ask-<nom>` et `/ask-with-context-<nom>` marchent toujours, et gardent l'échange dans l'historique. Voir [Interroger un autre agent mm](ask-another-agent.md).
65+- **Travail long.** Chaque événement diffusé par l'agent maintient en vie le watchdog de votre agent, mais pas un long silence — un gros modèle qui se charge, un long premier prompt : relevez votre `watchdogTimeout` pour le couvrir (10m pour un modèle de 48 Go sur un portable).
66+- **Depuis un script.** `mm --ask <nom>` reste la voie pour une commande shell. Voir [Interroger un autre agent depuis une skill ou un script](ask-another-agent-from-a-skill.md).
67+- **Exemples complets.** `demo-llmman/agent.llmman.yaml` (Bob avec Jan, Lucy et un relecteur) et `mini-mes/riker/agent.riker.yaml` (un coordinateur, un spécificateur et un codeur).
68+
69+## Voir aussi
70+
71+- Les champs de l'outil, son affichage et ses erreurs : [référence des outils](../reference/tools.md#ask_agent)
72+- Les deux formes de `mmConfigs` : [référence de la configuration](../reference/configuration.md)
73+- Pourquoi il tourne dans le tour plutôt que par une skill : [interroger un autre agent — explication](../explanation/asking-another-agent.md)
modified docs/fr/reference/configuration.md +2 -2
@@ -33,7 +33,7 @@
3333 | `tui` | bool | `false` | Démarre dans le mode fenêtre unique sans l'option `-tui`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal ; ignoré avec `-acp`. |
3434 | `yolo` | booléen | `true` | `true` : les outils s'exécutent sans demander. `false` : chaque appel qui modifie quelque chose — `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp`, `create`/`update`/`next`/`delete` de `plan` — attend la réponse de l'utilisateur dans toutes les façades (mode ligne, fenêtre, éditeur), de même que chaque outil MCP, quoi que dise `mcp.permissions`. Réponses : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, jamais (cet outil, pour la session). `read_file`, `read_skill`, `fetch_content` sans `output` et les actions de navigation de `lsp` ne demandent jamais. Avec `-p` et `mm --ask`, personne ne peut répondre : ces appels sont refusés. Un agent lancé par `/ask` tourne avec `AGENT_YOLO=false` et soumet ses appels à l'agent qui l'interroge, dont le `yolo` décide. Surchargée par `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` décident toujours quels outils existent. Voir [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). |
3535 | `permissions` | chaîne | absente | Obsolète, remplacée par `yolo`. `ask` = `yolo: false` (et l'emporte sur `yolo: true`) ; `auto` ne change rien. Les deux valeurs ajoutent un avertissement au démarrage. Doit valoir `auto` ou `ask`. |
36-| `mmConfigs` | dictionnaire nom → chemin | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)). Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. |
36+| `mmConfigs` | dictionnaire nom → chemin, ou nom → `{path, description, yolo}` | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)), et la liste déclare l'outil [`ask_agent`](tools.md#ask_agent), par lequel le modèle les appelle lui-même. La forme longue ajoute `description` (quand appeler cet agent ; affichée dans la description d'`ask_agent`, à défaut l'`acp.title` du fichier de l'agent) et `yolo` (décide des appels d'outils de cet agent quand il est interrogé, par `/ask`, `ask_agent` ou `mm --ask`, à la place du `yolo` de cet agent-ci : `true` ils s'exécutent, `false` chacun est demandé ; absent, le `yolo` de cet agent-ci décide). `path` ne doit pas être vide. Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. |
3737
3838 ## Clés de `context`
3939
@@ -124,7 +124,7 @@ Signalées sous la forme `[config error: <chemin>: <message>]`, code de sortie 1
124124 | `bashTimeout must be > 0` | zéro ou négative |
125125 | `mmConfigs: "Bob" is not a valid name (…)` | un nom avec des majuscules, des espaces ou d'autres caractères, ou commençant par `-` |
126126 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` |
127-| `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""` |
127+| `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""`, ou une forme longue sans `path` |
128128 | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Signalée par le moteur, sous la forme `[engine error: …]`. |
129129
130130 Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`, et quand le fichier pose la clé obsolète `permissions` (`permissions: ask is deprecated — use yolo: false instead …`).
@@ -33,7 +33,7 @@
33 | `tui` | bool | `false` | Démarre dans le mode fenêtre unique sans l'option `-tui`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal ; ignoré avec `-acp`. |33 | `tui` | bool | `false` | Démarre dans le mode fenêtre unique sans l'option `-tui`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal ; ignoré avec `-acp`. |
34 | `yolo` | booléen | `true` | `true` : les outils s'exécutent sans demander. `false` : chaque appel qui modifie quelque chose — `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp`, `create`/`update`/`next`/`delete` de `plan` — attend la réponse de l'utilisateur dans toutes les façades (mode ligne, fenêtre, éditeur), de même que chaque outil MCP, quoi que dise `mcp.permissions`. Réponses : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, jamais (cet outil, pour la session). `read_file`, `read_skill`, `fetch_content` sans `output` et les actions de navigation de `lsp` ne demandent jamais. Avec `-p` et `mm --ask`, personne ne peut répondre : ces appels sont refusés. Un agent lancé par `/ask` tourne avec `AGENT_YOLO=false` et soumet ses appels à l'agent qui l'interroge, dont le `yolo` décide. Surchargée par `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` décident toujours quels outils existent. Voir [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). |34 | `yolo` | booléen | `true` | `true` : les outils s'exécutent sans demander. `false` : chaque appel qui modifie quelque chose — `bash`, `write_file`, `edit_file`, `fetch_content` avec `output`, `rename` de `lsp`, `create`/`update`/`next`/`delete` de `plan` — attend la réponse de l'utilisateur dans toutes les façades (mode ligne, fenêtre, éditeur), de même que chaque outil MCP, quoi que dise `mcp.permissions`. Réponses : autoriser une fois, autoriser cet outil pour la session, refuser cet appel, jamais (cet outil, pour la session). `read_file`, `read_skill`, `fetch_content` sans `output` et les actions de navigation de `lsp` ne demandent jamais. Avec `-p` et `mm --ask`, personne ne peut répondre : ces appels sont refusés. Un agent lancé par `/ask` tourne avec `AGENT_YOLO=false` et soumet ses appels à l'agent qui l'interroge, dont le `yolo` décide. Surchargée par `AGENT_YOLO`. `bashTool`, `editTools`, `fetchContentTool` décident toujours quels outils existent. Voir [Confirmer les appels d'outils](../how-to/confirm-tool-calls.md). |
35 | `permissions` | chaîne | absente | Obsolète, remplacée par `yolo`. `ask` = `yolo: false` (et l'emporte sur `yolo: true`) ; `auto` ne change rien. Les deux valeurs ajoutent un avertissement au démarrage. Doit valoir `auto` ou `ask`. |35 | `permissions` | chaîne | absente | Obsolète, remplacée par `yolo`. `ask` = `yolo: false` (et l'emporte sur `yolo: true`) ; `auto` ne change rien. Les deux valeurs ajoutent un avertissement au démarrage. Doit valoir `auto` ou `ask`. |
36-| `mmConfigs` | dictionnaire nom → chemin | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)). Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. |36+| `mmConfigs` | dictionnaire nom → chemin, ou nom → `{path, description, yolo}` | aucun | Les autres agents mm que celui-ci peut interroger. Chaque nom génère `/ask-<nom>` et `/ask-with-context-<nom>` (voir les [commandes slash](slash-commands.md#interroger-un-autre-agent)), et la liste déclare l'outil [`ask_agent`](tools.md#ask_agent), par lequel le modèle les appelle lui-même. La forme longue ajoute `description` (quand appeler cet agent ; affichée dans la description d'`ask_agent`, à défaut l'`acp.title` du fichier de l'agent) et `yolo` (décide des appels d'outils de cet agent quand il est interrogé, par `/ask`, `ask_agent` ou `mm --ask`, à la place du `yolo` de cet agent-ci : `true` ils s'exécutent, `false` chacun est demandé ; absent, le `yolo` de cet agent-ci décide). `path` ne doit pas être vide. Un chemin relatif l'est au dossier du fichier qui le déclare. Noms : lettres minuscules, chiffres et `-`, commençant par une lettre ou un chiffre, ne commençant pas par `with-context`. Un chemin dont le fichier n'existe pas donne un avertissement au démarrage, pas une erreur. |
37 37
38 ## Clés de `context`38 ## Clés de `context`
39 39
@@ -124,7 +124,7 @@ Signalées sous la forme `[config error: <chemin>: <message>]`, code de sortie 1
124 | `bashTimeout must be > 0` | zéro ou négative |124 | `bashTimeout must be > 0` | zéro ou négative |
125 | `mmConfigs: "Bob" is not a valid name (…)` | un nom avec des majuscules, des espaces ou d'autres caractères, ou commençant par `-` |125 | `mmConfigs: "Bob" is not a valid name (…)` | un nom avec des majuscules, des espaces ou d'autres caractères, ou commençant par `-` |
126 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` |126 | `mmConfigs: "with-context-x" is not a valid name (it would read as /ask-with-context-…)` | un nom commençant par `with-context` |
127-| `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""` |127+| `mmConfigs.<nom>: the path must not be empty` | `<nom>: ""`, ou une forme longue sans `path` |
128 | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Signalée par le moteur, sous la forme `[engine error: …]`. |128 | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Signalée par le moteur, sous la forme `[engine error: …]`. |
129 129
130 Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`, et quand le fichier pose la clé obsolète `permissions` (`permissions: ask is deprecated — use yolo: false instead …`).130 Un avertissement au démarrage, et non une erreur, est affiché quand `context.enabled` vaut `true`, que la fenêtre est inconnue et que `context.maxMessages` vaut `0`, et quand le fichier pose la clé obsolète `permissions` (`permissions: ask is deprecated — use yolo: false instead …`).
modified docs/fr/reference/tools.md +22 -0
@@ -200,6 +200,28 @@ Déclaré seulement avec `fetchContentTool: true`. Télécharge une page web et
200200 | `2` | `input` | Hôte injoignable, délai dépassé, statut HTTP ≥ 300 après redirections, plus de 10 redirections, page interdite par le robots.txt ou robots.txt illisible. |
201201 | `3` | `usage` | `url` vide ou illisible, schéma autre que `http`/`https`, valeur de `tables` inconnue, `baseUrl` non absolue. |
202202
203+## `ask_agent`
204+
205+Déclaré quand `mmConfigs` nomme au moins un agent. Confie une tâche à l'un d'eux et renvoie sa réponse comme résultat de l'outil : c'est le modèle qui décide quand, d'après les descriptions des agents. Même mécanique que `/ask` (paquet `internal/ask`) : l'agent tourne comme un `mm -acp` enfant, démarré au premier appel puis réutilisé, avec une session neuve à chaque appel.
206+
207+| Champ | Type | Défaut | Description |
208+|-------|------|--------|-------------|
209+| `agent` | chaîne | — | Un nom de `mmConfigs`. |
210+| `prompt` | chaîne | — | La tâche, complète et autonome. |
211+| `with_context` | booléen | `false` | Envoie cette conversation avant le prompt : l'historique au début du tour (questions, réponses, une ligne par appel d'outil, jamais les sorties d'outils), limité à `maxOutput` caractères, les plus récents gardés. |
212+
213+| Aspect | Comportement |
214+|--------|--------------|
215+| Description | Un paragraphe fixe, puis `Agents:` et une ligne ` <nom> — <rôle>` par agent, triés par nom. `<rôle>` est la `description` de l'entrée, sinon l'`acp.title` du fichier de l'agent (s'il n'est pas celui intégré), sinon `(no description)`. |
216+| Permission | L'appel lui-même est soumis comme `bash` : demandé quand `yolo: false`. |
217+| Appels de l'agent interrogé | Décidés par le `yolo` de son entrée dans `mmConfigs` s'il est fixé (`true` exécutés, `false` demandés), sinon par le `yolo` de cet agent. |
218+| Répertoire de travail | Le répertoire du processus dans le terminal et la fenêtre ; le `cwd` de la session en ACP. |
219+| Affichage | Le texte, les pensées et les appels d'outils de l'agent interrogé défilent dans le tour, ses appels titrés `<agent> · <titre>`. La fenêtre le nomme pendant qu'il travaille : en-tête `mm [<acp.title>] · <modèle> @ <fournisseur> · asked by <cet agent>`, sa réponse sous `‣ mm [<acp.title>]` (son nom dans `mmConfigs` s'il n'a pas de titre). Mode ligne : `🗣 ask_agent: <agent> (<fournisseur>/<modèle>) — <première ligne du prompt>`, puis la sortie de l'agent. |
220+| Chien de garde | Chaque événement de l'agent interrogé (texte, pensée, appel d'outil, permission répondue) compte comme un signe de vie pour le chien de garde de cet agent. Un silence plus long que `watchdogTimeout` — un modèle qui se charge, un long prompt en lecture — termine quand même le tour. |
221+| Résultat | La réponse, tronquée à `maxOutput`. La sortie de l'appel lui-même dans la fenêtre et l'éditeur est `<agent> answered (N characters)` : la réponse a déjà défilé. Pas ajoutée à l'historique comme un échange `/ask` : c'est un résultat d'outil. |
222+| Erreurs | `[ask_agent: unknown agent "x" — agents: a, b]` ; `[ask_agent: the prompt is empty — say what <agent> must do]` ; `[ask_agent: <agent> failed: <raison>]` ; `[ask_agent: <agent> gave no answer]` ; une exécution annulée ou interrompue renvoie ce qui a été répondu, puis `[ask_agent: <agent> stopped before finishing: <raison>]`. |
223+| Profondeur | Un agent interrogé peut lui-même avoir des `mmConfigs` et appeler `ask_agent`, jusqu'à deux niveaux (`MM_ASK_DEPTH`) ; au-delà, l'appel échoue avec le message de profondeur. |
224+
203225 ## Détection de boucle
204226
205227 Chaque outil enregistre `(outil, entrée, sortie)` dans le détecteur de boucle après chaque appel.
@@ -200,6 +200,28 @@ Déclaré seulement avec `fetchContentTool: true`. Télécharge une page web et
200 | `2` | `input` | Hôte injoignable, délai dépassé, statut HTTP ≥ 300 après redirections, plus de 10 redirections, page interdite par le robots.txt ou robots.txt illisible. |200 | `2` | `input` | Hôte injoignable, délai dépassé, statut HTTP ≥ 300 après redirections, plus de 10 redirections, page interdite par le robots.txt ou robots.txt illisible. |
201 | `3` | `usage` | `url` vide ou illisible, schéma autre que `http`/`https`, valeur de `tables` inconnue, `baseUrl` non absolue. |201 | `3` | `usage` | `url` vide ou illisible, schéma autre que `http`/`https`, valeur de `tables` inconnue, `baseUrl` non absolue. |
202 202
203+## `ask_agent`
204+
205+Déclaré quand `mmConfigs` nomme au moins un agent. Confie une tâche à l'un d'eux et renvoie sa réponse comme résultat de l'outil : c'est le modèle qui décide quand, d'après les descriptions des agents. Même mécanique que `/ask` (paquet `internal/ask`) : l'agent tourne comme un `mm -acp` enfant, démarré au premier appel puis réutilisé, avec une session neuve à chaque appel.
206+
207+| Champ | Type | Défaut | Description |
208+|-------|------|--------|-------------|
209+| `agent` | chaîne | — | Un nom de `mmConfigs`. |
210+| `prompt` | chaîne | — | La tâche, complète et autonome. |
211+| `with_context` | booléen | `false` | Envoie cette conversation avant le prompt : l'historique au début du tour (questions, réponses, une ligne par appel d'outil, jamais les sorties d'outils), limité à `maxOutput` caractères, les plus récents gardés. |
212+
213+| Aspect | Comportement |
214+|--------|--------------|
215+| Description | Un paragraphe fixe, puis `Agents:` et une ligne ` <nom> — <rôle>` par agent, triés par nom. `<rôle>` est la `description` de l'entrée, sinon l'`acp.title` du fichier de l'agent (s'il n'est pas celui intégré), sinon `(no description)`. |
216+| Permission | L'appel lui-même est soumis comme `bash` : demandé quand `yolo: false`. |
217+| Appels de l'agent interrogé | Décidés par le `yolo` de son entrée dans `mmConfigs` s'il est fixé (`true` exécutés, `false` demandés), sinon par le `yolo` de cet agent. |
218+| Répertoire de travail | Le répertoire du processus dans le terminal et la fenêtre ; le `cwd` de la session en ACP. |
219+| Affichage | Le texte, les pensées et les appels d'outils de l'agent interrogé défilent dans le tour, ses appels titrés `<agent> · <titre>`. La fenêtre le nomme pendant qu'il travaille : en-tête `mm [<acp.title>] · <modèle> @ <fournisseur> · asked by <cet agent>`, sa réponse sous `‣ mm [<acp.title>]` (son nom dans `mmConfigs` s'il n'a pas de titre). Mode ligne : `🗣 ask_agent: <agent> (<fournisseur>/<modèle>) — <première ligne du prompt>`, puis la sortie de l'agent. |
220+| Chien de garde | Chaque événement de l'agent interrogé (texte, pensée, appel d'outil, permission répondue) compte comme un signe de vie pour le chien de garde de cet agent. Un silence plus long que `watchdogTimeout` — un modèle qui se charge, un long prompt en lecture — termine quand même le tour. |
221+| Résultat | La réponse, tronquée à `maxOutput`. La sortie de l'appel lui-même dans la fenêtre et l'éditeur est `<agent> answered (N characters)` : la réponse a déjà défilé. Pas ajoutée à l'historique comme un échange `/ask` : c'est un résultat d'outil. |
222+| Erreurs | `[ask_agent: unknown agent "x" — agents: a, b]` ; `[ask_agent: the prompt is empty — say what <agent> must do]` ; `[ask_agent: <agent> failed: <raison>]` ; `[ask_agent: <agent> gave no answer]` ; une exécution annulée ou interrompue renvoie ce qui a été répondu, puis `[ask_agent: <agent> stopped before finishing: <raison>]`. |
223+| Profondeur | Un agent interrogé peut lui-même avoir des `mmConfigs` et appeler `ask_agent`, jusqu'à deux niveaux (`MM_ASK_DEPTH`) ; au-delà, l'appel échoue avec le message de profondeur. |
224+
203 ## Détection de boucle225 ## Détection de boucle
204 226
205 Chaque outil enregistre `(outil, entrée, sortie)` dans le détecteur de boucle après chaque appel.227 Chaque outil enregistre `(outil, entrée, sortie)` dans le détecteur de boucle après chaque appel.
modified internal/acp/ask_test.go +1 -1
@@ -28,7 +28,7 @@ func withBob(t *testing.T) {
2828 t.Helper()
2929 saved := config.Cfg
3030 t.Cleanup(func() { config.Cfg = saved; command.SetAgents(nil) })
31- config.Cfg.MMConfigs = map[string]string{"bob": "/nowhere/agent.bob.yaml"}
31+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "/nowhere/agent.bob.yaml"}}
3232 command.SetAgents([]string{"bob"})
3333 }
3434
@@ -28,7 +28,7 @@ func withBob(t *testing.T) {
28 t.Helper()28 t.Helper()
29 saved := config.Cfg29 saved := config.Cfg
30 t.Cleanup(func() { config.Cfg = saved; command.SetAgents(nil) })30 t.Cleanup(func() { config.Cfg = saved; command.SetAgents(nil) })
31- config.Cfg.MMConfigs = map[string]string{"bob": "/nowhere/agent.bob.yaml"}31+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "/nowhere/agent.bob.yaml"}}
32 command.SetAgents([]string{"bob"})32 command.SetAgents([]string{"bob"})
33 }33 }
34 34
modified internal/agent/ask.go +1 -47
@@ -7,11 +7,7 @@ import (
77
88 "mm/internal/ask"
99 "mm/internal/command"
10- "mm/internal/config"
11- "mm/internal/consent"
12- "mm/internal/spinner"
1310 "mm/internal/store"
14- "mm/internal/ui"
1511
1612 "github.com/firebase/genkit/go/ai"
1713 )
@@ -41,7 +37,7 @@ func (a asking) run(ctx context.Context, req command.AskRequest, messages []*ai.
4137 }
4238 done := make(chan result, 1)
4339 go func() {
44- exchange, err := a.pool.Run(askCtx, req, messages, a.cwd, &lineSink{cwd: a.cwd})
40+ exchange, err := a.pool.Run(askCtx, req, messages, a.cwd, &ask.LineSink{Cwd: a.cwd})
4541 done <- result{exchange, err}
4642 }()
4743
@@ -92,45 +88,3 @@ func (a asking) settle(askCtx context.Context, req command.AskRequest, messages,
9288 dimln(fmt.Sprintf("🗣 %s's answer is in the history for your next question", req.Agent))
9389 return messages
9490 }
95-
96-// lineSink shows the other agent in the line mode: its answer as it streams,
97-// its thoughts dimmed, one line per tool call. Its calls reach Allow only
98-// when this agent's yolo is off (consent.Gate, in internal/ask): they are
99-// then asked through the REPL's prompt, like the line mode's own tools.
100-type lineSink struct {
101- cwd string
102-}
103-
104-var _ ui.Sink = (*lineSink)(nil)
105-
106-func (s *lineSink) Text(text string) {
107- spinner.Stop()
108- fmt.Print(text)
109-}
110-
111-func (s *lineSink) Reasoning(text string) {
112- if !config.Cfg.ShowThinking {
113- return
114- }
115- spinner.Stop()
116- if spinner.Styled() {
117- fmt.Printf("\033[2;3m%s\033[0m", text)
118- return
119- }
120- fmt.Print(text)
121-}
122-
123-func (s *lineSink) ToolStart(ev ui.ToolEvent) {
124- spinner.Stop()
125- fmt.Printf("\n🛠️ %s\n", ev.Title)
126-}
127-
128-func (s *lineSink) ToolEnd(_ string, res ui.ToolResult) {
129- if res.Failed {
130- dimln(" │ failed")
131- }
132-}
133-
134-func (s *lineSink) Allow(ctx context.Context, ev ui.ToolEvent) bool { return consent.Prompt(ctx, ev) }
135-func (s *lineSink) ToolRunning(string) {}
136-func (s *lineSink) WorkDir() string { return s.cwd }
@@ -7,11 +7,7 @@ import (
7 7
8 "mm/internal/ask"8 "mm/internal/ask"
9 "mm/internal/command"9 "mm/internal/command"
10- "mm/internal/config"
11- "mm/internal/consent"
12- "mm/internal/spinner"
13 "mm/internal/store"10 "mm/internal/store"
14- "mm/internal/ui"
15 11
16 "github.com/firebase/genkit/go/ai"12 "github.com/firebase/genkit/go/ai"
17 )13 )
@@ -41,7 +37,7 @@ func (a asking) run(ctx context.Context, req command.AskRequest, messages []*ai.
41 }37 }
42 done := make(chan result, 1)38 done := make(chan result, 1)
43 go func() {39 go func() {
44- exchange, err := a.pool.Run(askCtx, req, messages, a.cwd, &lineSink{cwd: a.cwd})40+ exchange, err := a.pool.Run(askCtx, req, messages, a.cwd, &ask.LineSink{Cwd: a.cwd})
45 done <- result{exchange, err}41 done <- result{exchange, err}
46 }()42 }()
47 43
@@ -92,45 +88,3 @@ func (a asking) settle(askCtx context.Context, req command.AskRequest, messages,
92 dimln(fmt.Sprintf("🗣 %s's answer is in the history for your next question", req.Agent))88 dimln(fmt.Sprintf("🗣 %s's answer is in the history for your next question", req.Agent))
93 return messages89 return messages
94 }90 }
95-
96-// lineSink shows the other agent in the line mode: its answer as it streams,
97-// its thoughts dimmed, one line per tool call. Its calls reach Allow only
98-// when this agent's yolo is off (consent.Gate, in internal/ask): they are
99-// then asked through the REPL's prompt, like the line mode's own tools.
100-type lineSink struct {
101- cwd string
102-}
103-
104-var _ ui.Sink = (*lineSink)(nil)
105-
106-func (s *lineSink) Text(text string) {
107- spinner.Stop()
108- fmt.Print(text)
109-}
110-
111-func (s *lineSink) Reasoning(text string) {
112- if !config.Cfg.ShowThinking {
113- return
114- }
115- spinner.Stop()
116- if spinner.Styled() {
117- fmt.Printf("\033[2;3m%s\033[0m", text)
118- return
119- }
120- fmt.Print(text)
121-}
122-
123-func (s *lineSink) ToolStart(ev ui.ToolEvent) {
124- spinner.Stop()
125- fmt.Printf("\n🛠️ %s\n", ev.Title)
126-}
127-
128-func (s *lineSink) ToolEnd(_ string, res ui.ToolResult) {
129- if res.Failed {
130- dimln(" │ failed")
131- }
132-}
133-
134-func (s *lineSink) Allow(ctx context.Context, ev ui.ToolEvent) bool { return consent.Prompt(ctx, ev) }
135-func (s *lineSink) ToolRunning(string) {}
136-func (s *lineSink) WorkDir() string { return s.cwd }
modified internal/agent/ask_test.go +1 -1
@@ -39,7 +39,7 @@ func lineAsk(t *testing.T, r fakeRunner) (asking, chan os.Signal, chan string) {
3939 saved := config.Cfg
4040 t.Cleanup(func() { config.Cfg = saved })
4141 config.Cfg.Sessions.Enabled = false
42- config.Cfg.MMConfigs = map[string]string{"bob": "/nowhere/agent.bob.yaml"}
42+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "/nowhere/agent.bob.yaml"}}
4343 sig, in := make(chan os.Signal, 1), make(chan string, 1)
4444 return asking{pool: r, cwd: t.TempDir(), keep: store.NewKeeper(t.TempDir()), sigCh: sig, inputCh: in}, sig, in
4545 }
@@ -39,7 +39,7 @@ func lineAsk(t *testing.T, r fakeRunner) (asking, chan os.Signal, chan string) {
39 saved := config.Cfg39 saved := config.Cfg
40 t.Cleanup(func() { config.Cfg = saved })40 t.Cleanup(func() { config.Cfg = saved })
41 config.Cfg.Sessions.Enabled = false41 config.Cfg.Sessions.Enabled = false
42- config.Cfg.MMConfigs = map[string]string{"bob": "/nowhere/agent.bob.yaml"}42+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "/nowhere/agent.bob.yaml"}}
43 sig, in := make(chan os.Signal, 1), make(chan string, 1)43 sig, in := make(chan os.Signal, 1), make(chan string, 1)
44 return asking{pool: r, cwd: t.TempDir(), keep: store.NewKeeper(t.TempDir()), sigCh: sig, inputCh: in}, sig, in44 return asking{pool: r, cwd: t.TempDir(), keep: store.NewKeeper(t.TempDir()), sigCh: sig, inputCh: in}, sig, in
45 }45 }
modified internal/ask/cli.go +2 -1
@@ -79,10 +79,11 @@ func ReadCLIRequest(agent string, withContext bool, prompt string, stdin io.Read
7979 // The other agent's tool calls are allowed, as in the line mode: the call to
8080 // `mm --ask` is itself the command a person (or a permission prompt) let run.
8181 func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stderr io.Writer) error {
82- path, ok := config.Cfg.MMConfigs[req.Agent]
82+ ref, ok := config.Cfg.MMConfigs[req.Agent]
8383 if !ok {
8484 return fmt.Errorf("unknown agent %q — agents in this config: %s", req.Agent, agentList())
8585 }
86+ path := ref.Path
8687 label := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(path))
8788 if req.WithContext {
8889 label += ", with the conversation"
@@ -79,10 +79,11 @@ func ReadCLIRequest(agent string, withContext bool, prompt string, stdin io.Read
79 // The other agent's tool calls are allowed, as in the line mode: the call to79 // The other agent's tool calls are allowed, as in the line mode: the call to
80 // `mm --ask` is itself the command a person (or a permission prompt) let run.80 // `mm --ask` is itself the command a person (or a permission prompt) let run.
81 func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stderr io.Writer) error {81 func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stderr io.Writer) error {
82- path, ok := config.Cfg.MMConfigs[req.Agent]82+ ref, ok := config.Cfg.MMConfigs[req.Agent]
83 if !ok {83 if !ok {
84 return fmt.Errorf("unknown agent %q — agents in this config: %s", req.Agent, agentList())84 return fmt.Errorf("unknown agent %q — agents in this config: %s", req.Agent, agentList())
85 }85 }
86+ path := ref.Path
86 label := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(path))87 label := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(path))
87 if req.WithContext {88 if req.WithContext {
88 label += ", with the conversation"89 label += ", with the conversation"
modified internal/ask/client.go +22 -4
@@ -5,6 +5,7 @@ import (
55 "strings"
66 "sync"
77
8+ "mm/internal/config"
89 "mm/internal/consent"
910 "mm/internal/ui"
1011
@@ -128,9 +129,10 @@ func (c *client) toolUpdate(sink ui.Sink, up *sdk.SessionToolCallUpdate) {
128129 }
129130
130131 // RequestPermission decides, for the asker, whether the child may run a call:
131-// the asker's yolo first (consent.Gate — the child runs with yolo off, so
132-// every call it would gate lands here), then the asker's user through the
133-// asker's front end. The answer is one of the options the child offered
132+// the child's own entry in mmConfigs first (its `yolo`, when set), else the
133+// asker's yolo (consent.Gate — the child runs with yolo off, so every call
134+// it would gate lands here), then the asker's user through the asker's front
135+// end. The answer is one of the options the child offered
134136 // (allow once / reject once — the asker remembers session answers, not the
135137 // child), or "cancelled" when it offered none that fits.
136138 func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error) {
@@ -147,7 +149,7 @@ func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionR
147149 if c.firstSight(p.ToolCall.ToolCallId) {
148150 sink.ToolStart(ev) // the request overtook the tool_call: show it first
149151 }
150- allowed = consent.Gate(ctx, sink, ev)
152+ allowed = c.gate(ctx, sink, ev)
151153 }
152154 want := []sdk.PermissionOptionKind{sdk.PermissionOptionKindRejectOnce, sdk.PermissionOptionKindRejectAlways}
153155 if allowed {
@@ -163,6 +165,22 @@ func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionR
163165 return sdk.RequestPermissionResponse{Outcome: sdk.RequestPermissionOutcome{Cancelled: &sdk.RequestPermissionOutcomeCancelled{}}}, nil
164166 }
165167
168+// gate applies the asked agent's own yolo when mmConfigs gives it one —
169+// true: its calls run; false: each one is asked, whatever the asker's yolo —
170+// and the asker's yolo otherwise.
171+func (c *client) gate(ctx context.Context, sink ui.Sink, ev ui.ToolEvent) bool {
172+ c.mu.Lock()
173+ agent := c.agent
174+ c.mu.Unlock()
175+ if yolo := config.Cfg.MMConfigs[agent].Yolo; yolo != nil {
176+ if *yolo {
177+ return true
178+ }
179+ return sink.Allow(ctx, ev)
180+ }
181+ return consent.Gate(ctx, sink, ev)
182+}
183+
166184 // event builds the ui.ToolEvent the asker shows for one of the child's calls.
167185 // Tool is "<agent>/<tool>" so that "allow for this session" in the window
168186 // grants bob's bash, not the asker's.
@@ -5,6 +5,7 @@ import (
5 "strings"5 "strings"
6 "sync"6 "sync"
7 7
8+ "mm/internal/config"
8 "mm/internal/consent"9 "mm/internal/consent"
9 "mm/internal/ui"10 "mm/internal/ui"
10 11
@@ -128,9 +129,10 @@ func (c *client) toolUpdate(sink ui.Sink, up *sdk.SessionToolCallUpdate) {
128 }129 }
129 130
130 // RequestPermission decides, for the asker, whether the child may run a call:131 // RequestPermission decides, for the asker, whether the child may run a call:
131-// the asker's yolo first (consent.Gate — the child runs with yolo off, so132+// the child's own entry in mmConfigs first (its `yolo`, when set), else the
132-// every call it would gate lands here), then the asker's user through the133+// asker's yolo (consent.Gate — the child runs with yolo off, so every call
133-// asker's front end. The answer is one of the options the child offered134+// it would gate lands here), then the asker's user through the asker's front
135+// end. The answer is one of the options the child offered
134 // (allow once / reject once — the asker remembers session answers, not the136 // (allow once / reject once — the asker remembers session answers, not the
135 // child), or "cancelled" when it offered none that fits.137 // child), or "cancelled" when it offered none that fits.
136 func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error) {138 func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionRequest) (sdk.RequestPermissionResponse, error) {
@@ -147,7 +149,7 @@ func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionR
147 if c.firstSight(p.ToolCall.ToolCallId) {149 if c.firstSight(p.ToolCall.ToolCallId) {
148 sink.ToolStart(ev) // the request overtook the tool_call: show it first150 sink.ToolStart(ev) // the request overtook the tool_call: show it first
149 }151 }
150- allowed = consent.Gate(ctx, sink, ev)152+ allowed = c.gate(ctx, sink, ev)
151 }153 }
152 want := []sdk.PermissionOptionKind{sdk.PermissionOptionKindRejectOnce, sdk.PermissionOptionKindRejectAlways}154 want := []sdk.PermissionOptionKind{sdk.PermissionOptionKindRejectOnce, sdk.PermissionOptionKindRejectAlways}
153 if allowed {155 if allowed {
@@ -163,6 +165,22 @@ func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionR
163 return sdk.RequestPermissionResponse{Outcome: sdk.RequestPermissionOutcome{Cancelled: &sdk.RequestPermissionOutcomeCancelled{}}}, nil165 return sdk.RequestPermissionResponse{Outcome: sdk.RequestPermissionOutcome{Cancelled: &sdk.RequestPermissionOutcomeCancelled{}}}, nil
164 }166 }
165 167
168+// gate applies the asked agent's own yolo when mmConfigs gives it one —
169+// true: its calls run; false: each one is asked, whatever the asker's yolo —
170+// and the asker's yolo otherwise.
171+func (c *client) gate(ctx context.Context, sink ui.Sink, ev ui.ToolEvent) bool {
172+ c.mu.Lock()
173+ agent := c.agent
174+ c.mu.Unlock()
175+ if yolo := config.Cfg.MMConfigs[agent].Yolo; yolo != nil {
176+ if *yolo {
177+ return true
178+ }
179+ return sink.Allow(ctx, ev)
180+ }
181+ return consent.Gate(ctx, sink, ev)
182+}
183+
166 // event builds the ui.ToolEvent the asker shows for one of the child's calls.184 // event builds the ui.ToolEvent the asker shows for one of the child's calls.
167 // Tool is "<agent>/<tool>" so that "allow for this session" in the window185 // Tool is "<agent>/<tool>" so that "allow for this session" in the window
168 // grants bob's bash, not the asker's.186 // grants bob's bash, not the asker's.
added internal/ask/linesink.go +60 -0
new file mode 100644
@@ -0,0 +1,60 @@
1+package ask
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "mm/internal/config"
8+ "mm/internal/consent"
9+ "mm/internal/spinner"
10+ "mm/internal/ui"
11+)
12+
13+// LineSink shows an asked agent in the line mode: its answer as it streams,
14+// its thoughts dimmed, one line per tool call. Its calls reach Allow only
15+// when they need the user (the asked agent's yolo, else this agent's): they
16+// are then asked through the REPL's prompt, like the line mode's own tools.
17+//
18+// answer, err := pool.Ask(ctx, "homer", path, cwd, prompt, &ask.LineSink{Cwd: cwd})
19+type LineSink struct {
20+ Cwd string
21+}
22+
23+var _ ui.Sink = (*LineSink)(nil)
24+
25+func (s *LineSink) Text(text string) {
26+ spinner.Stop()
27+ fmt.Print(text)
28+}
29+
30+func (s *LineSink) Reasoning(text string) {
31+ if !config.Cfg.ShowThinking {
32+ return
33+ }
34+ spinner.Stop()
35+ if spinner.Styled() {
36+ fmt.Printf("\033[2;3m%s\033[0m", text)
37+ return
38+ }
39+ fmt.Print(text)
40+}
41+
42+func (s *LineSink) ToolStart(ev ui.ToolEvent) {
43+ spinner.Stop()
44+ fmt.Printf("\n🛠️ %s\n", ev.Title)
45+}
46+
47+func (s *LineSink) ToolEnd(_ string, res ui.ToolResult) {
48+ if !res.Failed {
49+ return
50+ }
51+ if spinner.Styled() {
52+ fmt.Print("\033[2m │ failed\033[0m\n")
53+ return
54+ }
55+ fmt.Println(" │ failed")
56+}
57+
58+func (s *LineSink) Allow(ctx context.Context, ev ui.ToolEvent) bool { return consent.Prompt(ctx, ev) }
59+func (s *LineSink) ToolRunning(string) {}
60+func (s *LineSink) WorkDir() string { return s.Cwd }
new file mode 100644
@@ -0,0 +1,60 @@
1+package ask
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "mm/internal/config"
8+ "mm/internal/consent"
9+ "mm/internal/spinner"
10+ "mm/internal/ui"
11+)
12+
13+// LineSink shows an asked agent in the line mode: its answer as it streams,
14+// its thoughts dimmed, one line per tool call. Its calls reach Allow only
15+// when they need the user (the asked agent's yolo, else this agent's): they
16+// are then asked through the REPL's prompt, like the line mode's own tools.
17+//
18+// answer, err := pool.Ask(ctx, "homer", path, cwd, prompt, &ask.LineSink{Cwd: cwd})
19+type LineSink struct {
20+ Cwd string
21+}
22+
23+var _ ui.Sink = (*LineSink)(nil)
24+
25+func (s *LineSink) Text(text string) {
26+ spinner.Stop()
27+ fmt.Print(text)
28+}
29+
30+func (s *LineSink) Reasoning(text string) {
31+ if !config.Cfg.ShowThinking {
32+ return
33+ }
34+ spinner.Stop()
35+ if spinner.Styled() {
36+ fmt.Printf("\033[2;3m%s\033[0m", text)
37+ return
38+ }
39+ fmt.Print(text)
40+}
41+
42+func (s *LineSink) ToolStart(ev ui.ToolEvent) {
43+ spinner.Stop()
44+ fmt.Printf("\n🛠️ %s\n", ev.Title)
45+}
46+
47+func (s *LineSink) ToolEnd(_ string, res ui.ToolResult) {
48+ if !res.Failed {
49+ return
50+ }
51+ if spinner.Styled() {
52+ fmt.Print("\033[2m │ failed\033[0m\n")
53+ return
54+ }
55+ fmt.Println(" │ failed")
56+}
57+
58+func (s *LineSink) Allow(ctx context.Context, ev ui.ToolEvent) bool { return consent.Prompt(ctx, ev) }
59+func (s *LineSink) ToolRunning(string) {}
60+func (s *LineSink) WorkDir() string { return s.Cwd }
modified internal/ask/pool_test.go +24 -0
@@ -149,6 +149,30 @@ func TestAskerInYoloModeAllowsWithoutAsking(t *testing.T) {
149149 }
150150 }
151151
152+// The asked agent's own yolo in mmConfigs beats the asker's, both ways:
153+// true runs its calls with the asker's yolo off, false asks with it on.
154+func TestAgentYoloOverridesTheAskers(t *testing.T) {
155+ saved := config.Cfg.MMConfigs
156+ t.Cleanup(func() { config.Cfg.MMConfigs = saved })
157+ on, off := true, false
158+
159+ askerYolo(t, false)
160+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "x", Yolo: &on}}
161+ sink := &recorder{allow: false}
162+ answer, err := fakePool(t, "tool").Ask(context.Background(), "bob", configFile(t), t.TempDir(), "q", sink)
163+ if err != nil || !strings.HasSuffix(answer, "(tool allowed)") || strings.Contains(sink.all(), "allow?") {
164+ t.Errorf("bob yolo true, asker off: answer %q, err %v, events %s", answer, err, sink.all())
165+ }
166+
167+ config.Cfg.Yolo = true
168+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "x", Yolo: &off}}
169+ sink = &recorder{allow: false}
170+ answer, err = fakePool(t, "tool").Ask(context.Background(), "bob", configFile(t), t.TempDir(), "q", sink)
171+ if err != nil || !strings.HasSuffix(answer, "(tool rejected)") || !strings.Contains(sink.all(), "allow?") {
172+ t.Errorf("bob yolo false, asker on: answer %q, err %v, events %s", answer, err, sink.all())
173+ }
174+}
175+
152176 // The child is started once and reused, with a fresh session per ask.
153177 func TestAskReusesTheChildWithAFreshSession(t *testing.T) {
154178 p := fakePool(t, "echo")
@@ -149,6 +149,30 @@ func TestAskerInYoloModeAllowsWithoutAsking(t *testing.T) {
149 }149 }
150 }150 }
151 151
152+// The asked agent's own yolo in mmConfigs beats the asker's, both ways:
153+// true runs its calls with the asker's yolo off, false asks with it on.
154+func TestAgentYoloOverridesTheAskers(t *testing.T) {
155+ saved := config.Cfg.MMConfigs
156+ t.Cleanup(func() { config.Cfg.MMConfigs = saved })
157+ on, off := true, false
158+
159+ askerYolo(t, false)
160+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "x", Yolo: &on}}
161+ sink := &recorder{allow: false}
162+ answer, err := fakePool(t, "tool").Ask(context.Background(), "bob", configFile(t), t.TempDir(), "q", sink)
163+ if err != nil || !strings.HasSuffix(answer, "(tool allowed)") || strings.Contains(sink.all(), "allow?") {
164+ t.Errorf("bob yolo true, asker off: answer %q, err %v, events %s", answer, err, sink.all())
165+ }
166+
167+ config.Cfg.Yolo = true
168+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "x", Yolo: &off}}
169+ sink = &recorder{allow: false}
170+ answer, err = fakePool(t, "tool").Ask(context.Background(), "bob", configFile(t), t.TempDir(), "q", sink)
171+ if err != nil || !strings.HasSuffix(answer, "(tool rejected)") || !strings.Contains(sink.all(), "allow?") {
172+ t.Errorf("bob yolo false, asker on: answer %q, err %v, events %s", answer, err, sink.all())
173+ }
174+}
175+
152 // The child is started once and reused, with a fresh session per ask.176 // The child is started once and reused, with a fresh session per ask.
153 func TestAskReusesTheChildWithAFreshSession(t *testing.T) {177 func TestAskReusesTheChildWithAFreshSession(t *testing.T) {
154 p := fakePool(t, "echo")178 p := fakePool(t, "echo")
modified internal/ask/run.go +4 -3
@@ -36,10 +36,11 @@ var _ Runner = (*Pool)(nil)
3636 // }
3737 // }
3838 func (p *Pool) Run(ctx context.Context, req command.AskRequest, history []*ai.Message, cwd string, sink ui.Sink) ([]*ai.Message, error) {
39- path, ok := config.Cfg.MMConfigs[req.Agent]
39+ ref, ok := config.Cfg.MMConfigs[req.Agent]
4040 if !ok {
4141 return nil, fmt.Errorf("unknown agent %q", req.Agent)
4242 }
43+ path := ref.Path
4344 transcript := ""
4445 if req.WithContext {
4546 transcript = Transcript(history, config.Cfg.MaxOutput)
@@ -59,7 +60,7 @@ func (p *Pool) Run(ctx context.Context, req command.AskRequest, history []*ai.Me
5960 //
6061 // ask.Label(req) // "🗣 asking bob (ollama/qwen3.5:2b), with the conversation…"
6162 func Label(req command.AskRequest) string {
62- line := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(config.Cfg.MMConfigs[req.Agent]))
63+ line := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(config.Cfg.MMConfigs[req.Agent].Path))
6364 if req.WithContext {
6465 line += ", with the conversation"
6566 }
@@ -71,7 +72,7 @@ func Label(req command.AskRequest) string {
7172 func Missing() []string {
7273 var warnings []string
7374 for _, name := range config.AgentNames() {
74- if path := config.Cfg.MMConfigs[name]; !exists(path) {
75+ if path := config.Cfg.MMConfigs[name].Path; !exists(path) {
7576 warnings = append(warnings, fmt.Sprintf("mmConfigs: %s → %s not found — /ask-%s will fail", name, path, name))
7677 }
7778 }
@@ -36,10 +36,11 @@ var _ Runner = (*Pool)(nil)
36 // }36 // }
37 // }37 // }
38 func (p *Pool) Run(ctx context.Context, req command.AskRequest, history []*ai.Message, cwd string, sink ui.Sink) ([]*ai.Message, error) {38 func (p *Pool) Run(ctx context.Context, req command.AskRequest, history []*ai.Message, cwd string, sink ui.Sink) ([]*ai.Message, error) {
39- path, ok := config.Cfg.MMConfigs[req.Agent]39+ ref, ok := config.Cfg.MMConfigs[req.Agent]
40 if !ok {40 if !ok {
41 return nil, fmt.Errorf("unknown agent %q", req.Agent)41 return nil, fmt.Errorf("unknown agent %q", req.Agent)
42 }42 }
43+ path := ref.Path
43 transcript := ""44 transcript := ""
44 if req.WithContext {45 if req.WithContext {
45 transcript = Transcript(history, config.Cfg.MaxOutput)46 transcript = Transcript(history, config.Cfg.MaxOutput)
@@ -59,7 +60,7 @@ func (p *Pool) Run(ctx context.Context, req command.AskRequest, history []*ai.Me
59 //60 //
60 // ask.Label(req) // "🗣 asking bob (ollama/qwen3.5:2b), with the conversation…"61 // ask.Label(req) // "🗣 asking bob (ollama/qwen3.5:2b), with the conversation…"
61 func Label(req command.AskRequest) string {62 func Label(req command.AskRequest) string {
62- line := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(config.Cfg.MMConfigs[req.Agent]))63+ line := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(config.Cfg.MMConfigs[req.Agent].Path))
63 if req.WithContext {64 if req.WithContext {
64 line += ", with the conversation"65 line += ", with the conversation"
65 }66 }
@@ -71,7 +72,7 @@ func Label(req command.AskRequest) string {
71 func Missing() []string {72 func Missing() []string {
72 var warnings []string73 var warnings []string
73 for _, name := range config.AgentNames() {74 for _, name := range config.AgentNames() {
74- if path := config.Cfg.MMConfigs[name]; !exists(path) {75+ if path := config.Cfg.MMConfigs[name].Path; !exists(path) {
75 warnings = append(warnings, fmt.Sprintf("mmConfigs: %s → %s not found — /ask-%s will fail", name, path, name))76 warnings = append(warnings, fmt.Sprintf("mmConfigs: %s → %s not found — /ask-%s will fail", name, path, name))
76 }77 }
77 }78 }
modified internal/ask/run_test.go +2 -2
@@ -22,7 +22,7 @@ func withAgent(t *testing.T) {
2222 if err := os.WriteFile(path, []byte("provider: ollama\nmodel: qwen3.5:2b\n"), 0o644); err != nil {
2323 t.Fatal(err)
2424 }
25- config.Cfg.MMConfigs = map[string]string{"bob": path}
25+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: path}}
2626 }
2727
2828 // A plain ask sends the question alone and returns the marked pair.
@@ -68,7 +68,7 @@ func TestRunFailures(t *testing.T) {
6868 if len(Missing()) != 0 {
6969 t.Errorf("Missing() = %v with an existing file", Missing())
7070 }
71- config.Cfg.MMConfigs["riker"] = filepath.Join(t.TempDir(), "nope.yaml")
71+ config.Cfg.MMConfigs["riker"] = config.AgentRef{Path: filepath.Join(t.TempDir(), "nope.yaml")}
7272 if m := Missing(); len(m) != 1 || !strings.Contains(m[0], "riker") {
7373 t.Errorf("Missing() = %v", m)
7474 }
@@ -22,7 +22,7 @@ func withAgent(t *testing.T) {
22 if err := os.WriteFile(path, []byte("provider: ollama\nmodel: qwen3.5:2b\n"), 0o644); err != nil {22 if err := os.WriteFile(path, []byte("provider: ollama\nmodel: qwen3.5:2b\n"), 0o644); err != nil {
23 t.Fatal(err)23 t.Fatal(err)
24 }24 }
25- config.Cfg.MMConfigs = map[string]string{"bob": path}25+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: path}}
26 }26 }
27 27
28 // A plain ask sends the question alone and returns the marked pair.28 // A plain ask sends the question alone and returns the marked pair.
@@ -68,7 +68,7 @@ func TestRunFailures(t *testing.T) {
68 if len(Missing()) != 0 {68 if len(Missing()) != 0 {
69 t.Errorf("Missing() = %v with an existing file", Missing())69 t.Errorf("Missing() = %v with an existing file", Missing())
70 }70 }
71- config.Cfg.MMConfigs["riker"] = filepath.Join(t.TempDir(), "nope.yaml")71+ config.Cfg.MMConfigs["riker"] = config.AgentRef{Path: filepath.Join(t.TempDir(), "nope.yaml")}
72 if m := Missing(); len(m) != 1 || !strings.Contains(m[0], "riker") {72 if m := Missing(); len(m) != 1 || !strings.Contains(m[0], "riker") {
73 t.Errorf("Missing() = %v", m)73 t.Errorf("Missing() = %v", m)
74 }74 }
modified internal/config/agents.go +45 -6
@@ -6,6 +6,8 @@ import (
66 "regexp"
77 "sort"
88 "strings"
9+
10+ "gopkg.in/yaml.v3"
911 )
1012
1113 // SessionsEnv names the variable that turns session persistence off for one
@@ -30,7 +32,7 @@ func (c Config) validateAgents() error {
3032 if strings.HasPrefix(name, "with-context") {
3133 return fmt.Errorf("mmConfigs: %q is not a valid name (it would read as /ask-with-context-…)", name)
3234 }
33- if c.MMConfigs[name] == "" {
35+ if c.MMConfigs[name].Path == "" {
3436 return fmt.Errorf("mmConfigs.%s: the path must not be empty", name)
3537 }
3638 }
@@ -42,12 +44,13 @@ func (c Config) validateAgents() error {
4244 // not depend on who started mm (a terminal, an editor, another mm).
4345 func resolveAgentPaths(configPath string) {
4446 dir := filepath.Dir(configPath)
45- for name, p := range Cfg.MMConfigs {
46- if p == "" || filepath.IsAbs(p) {
47+ for name, ref := range Cfg.MMConfigs {
48+ if ref.Path == "" || filepath.IsAbs(ref.Path) {
4749 continue
4850 }
49- if abs, err := filepath.Abs(filepath.Join(dir, p)); err == nil {
50- Cfg.MMConfigs[name] = abs
51+ if abs, err := filepath.Abs(filepath.Join(dir, ref.Path)); err == nil {
52+ ref.Path = abs
53+ Cfg.MMConfigs[name] = ref
5154 }
5255 }
5356 }
@@ -56,7 +59,7 @@ func resolveAgentPaths(configPath string) {
5659 // editor's menu and of the window's picker.
5760 //
5861 // for _, name := range config.AgentNames() {
59-// fmt.Println("/ask-" + name, config.Cfg.MMConfigs[name])
62+// fmt.Println("/ask-" + name, config.Cfg.MMConfigs[name].Path)
6063 // }
6164 func AgentNames() []string {
6265 names := make([]string, 0, len(Cfg.MMConfigs))
@@ -66,3 +69,39 @@ func AgentNames() []string {
6669 sort.Strings(names)
6770 return names
6871 }
72+
73+// AgentRef is one entry of mmConfigs. Two spellings, the short one being the
74+// path alone:
75+//
76+// mmConfigs:
77+// jan: agent.jan-nano.yaml
78+// homer:
79+// path: agent.homer.yaml
80+// description: Implements a specification file, with tests.
81+// yolo: true
82+type AgentRef struct {
83+ // Path of the agent's config file.
84+ Path string `yaml:"path"`
85+ // Description says when to call this agent: the `ask_agent` tool shows
86+ // it to the model. Empty = the agent's acp.title, else its name alone.
87+ Description string `yaml:"description"`
88+ // Yolo decides THIS agent's tool calls when it is asked, instead of the
89+ // asking agent's yolo: true = they run, false = each one is asked. Absent
90+ // = the asking agent's yolo decides, as for any agent.
91+ Yolo *bool `yaml:"yolo"`
92+}
93+
94+// UnmarshalYAML reads either spelling of an AgentRef.
95+func (r *AgentRef) UnmarshalYAML(node *yaml.Node) error {
96+ if node.Kind == yaml.ScalarNode {
97+ *r = AgentRef{}
98+ return node.Decode(&r.Path)
99+ }
100+ type plain AgentRef // no UnmarshalYAML: no recursion
101+ var p plain
102+ if err := node.Decode(&p); err != nil {
103+ return err
104+ }
105+ *r = AgentRef(p)
106+ return nil
107+}
@@ -6,6 +6,8 @@ import (
6 "regexp"6 "regexp"
7 "sort"7 "sort"
8 "strings"8 "strings"
9+
10+ "gopkg.in/yaml.v3"
9 )11 )
10 12
11 // SessionsEnv names the variable that turns session persistence off for one13 // SessionsEnv names the variable that turns session persistence off for one
@@ -30,7 +32,7 @@ func (c Config) validateAgents() error {
30 if strings.HasPrefix(name, "with-context") {32 if strings.HasPrefix(name, "with-context") {
31 return fmt.Errorf("mmConfigs: %q is not a valid name (it would read as /ask-with-context-…)", name)33 return fmt.Errorf("mmConfigs: %q is not a valid name (it would read as /ask-with-context-…)", name)
32 }34 }
33- if c.MMConfigs[name] == "" {35+ if c.MMConfigs[name].Path == "" {
34 return fmt.Errorf("mmConfigs.%s: the path must not be empty", name)36 return fmt.Errorf("mmConfigs.%s: the path must not be empty", name)
35 }37 }
36 }38 }
@@ -42,12 +44,13 @@ func (c Config) validateAgents() error {
42 // not depend on who started mm (a terminal, an editor, another mm).44 // not depend on who started mm (a terminal, an editor, another mm).
43 func resolveAgentPaths(configPath string) {45 func resolveAgentPaths(configPath string) {
44 dir := filepath.Dir(configPath)46 dir := filepath.Dir(configPath)
45- for name, p := range Cfg.MMConfigs {47+ for name, ref := range Cfg.MMConfigs {
46- if p == "" || filepath.IsAbs(p) {48+ if ref.Path == "" || filepath.IsAbs(ref.Path) {
47 continue49 continue
48 }50 }
49- if abs, err := filepath.Abs(filepath.Join(dir, p)); err == nil {51+ if abs, err := filepath.Abs(filepath.Join(dir, ref.Path)); err == nil {
50- Cfg.MMConfigs[name] = abs52+ ref.Path = abs
53+ Cfg.MMConfigs[name] = ref
51 }54 }
52 }55 }
53 }56 }
@@ -56,7 +59,7 @@ func resolveAgentPaths(configPath string) {
56 // editor's menu and of the window's picker.59 // editor's menu and of the window's picker.
57 //60 //
58 // for _, name := range config.AgentNames() {61 // for _, name := range config.AgentNames() {
59-// fmt.Println("/ask-" + name, config.Cfg.MMConfigs[name])62+// fmt.Println("/ask-" + name, config.Cfg.MMConfigs[name].Path)
60 // }63 // }
61 func AgentNames() []string {64 func AgentNames() []string {
62 names := make([]string, 0, len(Cfg.MMConfigs))65 names := make([]string, 0, len(Cfg.MMConfigs))
@@ -66,3 +69,39 @@ func AgentNames() []string {
66 sort.Strings(names)69 sort.Strings(names)
67 return names70 return names
68 }71 }
72+
73+// AgentRef is one entry of mmConfigs. Two spellings, the short one being the
74+// path alone:
75+//
76+// mmConfigs:
77+// jan: agent.jan-nano.yaml
78+// homer:
79+// path: agent.homer.yaml
80+// description: Implements a specification file, with tests.
81+// yolo: true
82+type AgentRef struct {
83+ // Path of the agent's config file.
84+ Path string `yaml:"path"`
85+ // Description says when to call this agent: the `ask_agent` tool shows
86+ // it to the model. Empty = the agent's acp.title, else its name alone.
87+ Description string `yaml:"description"`
88+ // Yolo decides THIS agent's tool calls when it is asked, instead of the
89+ // asking agent's yolo: true = they run, false = each one is asked. Absent
90+ // = the asking agent's yolo decides, as for any agent.
91+ Yolo *bool `yaml:"yolo"`
92+}
93+
94+// UnmarshalYAML reads either spelling of an AgentRef.
95+func (r *AgentRef) UnmarshalYAML(node *yaml.Node) error {
96+ if node.Kind == yaml.ScalarNode {
97+ *r = AgentRef{}
98+ return node.Decode(&r.Path)
99+ }
100+ type plain AgentRef // no UnmarshalYAML: no recursion
101+ var p plain
102+ if err := node.Decode(&p); err != nil {
103+ return err
104+ }
105+ *r = AgentRef(p)
106+ return nil
107+}
modified internal/config/agents_test.go +29 -2
@@ -24,10 +24,10 @@ func TestMMConfigsAreResolvedAgainstTheConfigFile(t *testing.T) {
2424 if _, err := Load(path); err != nil {
2525 t.Fatal(err)
2626 }
27- if got, want := Cfg.MMConfigs["riker"], filepath.Join(dir, "other", "agent.riker.yaml"); got != want {
27+ if got, want := Cfg.MMConfigs["riker"].Path, filepath.Join(dir, "other", "agent.riker.yaml"); got != want {
2828 t.Errorf("riker = %q, want %q", got, want)
2929 }
30- if got := Cfg.MMConfigs["bob"]; got != "/abs/agent.bob.yaml" {
30+ if got := Cfg.MMConfigs["bob"].Path; got != "/abs/agent.bob.yaml" {
3131 t.Errorf("bob = %q, want the absolute path unchanged", got)
3232 }
3333 if got := AgentNames(); !reflect.DeepEqual(got, []string{"bob", "riker"}) {
@@ -35,6 +35,32 @@ func TestMMConfigsAreResolvedAgainstTheConfigFile(t *testing.T) {
3535 }
3636 }
3737
38+// The long spelling carries a description and a yolo, and its relative path
39+// is anchored like the short one's; yolo absent stays nil (the asker decides).
40+func TestMMConfigsLongForm(t *testing.T) {
41+ defaults := Cfg
42+ t.Cleanup(func() { Cfg = defaults })
43+ dir := t.TempDir()
44+ path := filepath.Join(dir, "agent.yaml")
45+ body := "mmConfigs:\n jan: agent.jan.yaml\n homer:\n path: agent.homer.yaml\n description: Implements a spec.\n yolo: true\n milo:\n path: agent.milo.yaml\n yolo: false\n"
46+ if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
47+ t.Fatal(err)
48+ }
49+ if _, err := Load(path); err != nil {
50+ t.Fatal(err)
51+ }
52+ homer := Cfg.MMConfigs["homer"]
53+ if homer.Path != filepath.Join(dir, "agent.homer.yaml") || homer.Description != "Implements a spec." || homer.Yolo == nil || !*homer.Yolo {
54+ t.Errorf("homer = %+v", homer)
55+ }
56+ if milo := Cfg.MMConfigs["milo"]; milo.Yolo == nil || *milo.Yolo {
57+ t.Errorf("milo yolo = %v, want false", milo.Yolo)
58+ }
59+ if jan := Cfg.MMConfigs["jan"]; jan.Path != filepath.Join(dir, "agent.jan.yaml") || jan.Description != "" || jan.Yolo != nil {
60+ t.Errorf("jan = %+v", jan)
61+ }
62+}
63+
3864 // A name that could not be a command name, or an empty path, is a config
3965 // error naming the culprit; no mmConfigs at all is fine.
4066 func TestMMConfigsValidation(t *testing.T) {
@@ -47,6 +73,7 @@ func TestMMConfigsValidation(t *testing.T) {
4773 {"mmConfigs:\n ask bob: a.yaml\n", `"ask bob" is not a valid name`},
4874 {"mmConfigs:\n -bob: a.yaml\n", `"-bob" is not a valid name`},
4975 {"mmConfigs:\n bob: \"\"\n", "mmConfigs.bob: the path must not be empty"},
76+ {"mmConfigs:\n bob:\n description: no path\n", "mmConfigs.bob: the path must not be empty"},
5077 {"mmConfigs:\n with-context-bob: a.yaml\n", "it would read as /ask-with-context"},
5178 } {
5279 Cfg = defaults
@@ -24,10 +24,10 @@ func TestMMConfigsAreResolvedAgainstTheConfigFile(t *testing.T) {
24 if _, err := Load(path); err != nil {24 if _, err := Load(path); err != nil {
25 t.Fatal(err)25 t.Fatal(err)
26 }26 }
27- if got, want := Cfg.MMConfigs["riker"], filepath.Join(dir, "other", "agent.riker.yaml"); got != want {27+ if got, want := Cfg.MMConfigs["riker"].Path, filepath.Join(dir, "other", "agent.riker.yaml"); got != want {
28 t.Errorf("riker = %q, want %q", got, want)28 t.Errorf("riker = %q, want %q", got, want)
29 }29 }
30- if got := Cfg.MMConfigs["bob"]; got != "/abs/agent.bob.yaml" {30+ if got := Cfg.MMConfigs["bob"].Path; got != "/abs/agent.bob.yaml" {
31 t.Errorf("bob = %q, want the absolute path unchanged", got)31 t.Errorf("bob = %q, want the absolute path unchanged", got)
32 }32 }
33 if got := AgentNames(); !reflect.DeepEqual(got, []string{"bob", "riker"}) {33 if got := AgentNames(); !reflect.DeepEqual(got, []string{"bob", "riker"}) {
@@ -35,6 +35,32 @@ func TestMMConfigsAreResolvedAgainstTheConfigFile(t *testing.T) {
35 }35 }
36 }36 }
37 37
38+// The long spelling carries a description and a yolo, and its relative path
39+// is anchored like the short one's; yolo absent stays nil (the asker decides).
40+func TestMMConfigsLongForm(t *testing.T) {
41+ defaults := Cfg
42+ t.Cleanup(func() { Cfg = defaults })
43+ dir := t.TempDir()
44+ path := filepath.Join(dir, "agent.yaml")
45+ body := "mmConfigs:\n jan: agent.jan.yaml\n homer:\n path: agent.homer.yaml\n description: Implements a spec.\n yolo: true\n milo:\n path: agent.milo.yaml\n yolo: false\n"
46+ if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
47+ t.Fatal(err)
48+ }
49+ if _, err := Load(path); err != nil {
50+ t.Fatal(err)
51+ }
52+ homer := Cfg.MMConfigs["homer"]
53+ if homer.Path != filepath.Join(dir, "agent.homer.yaml") || homer.Description != "Implements a spec." || homer.Yolo == nil || !*homer.Yolo {
54+ t.Errorf("homer = %+v", homer)
55+ }
56+ if milo := Cfg.MMConfigs["milo"]; milo.Yolo == nil || *milo.Yolo {
57+ t.Errorf("milo yolo = %v, want false", milo.Yolo)
58+ }
59+ if jan := Cfg.MMConfigs["jan"]; jan.Path != filepath.Join(dir, "agent.jan.yaml") || jan.Description != "" || jan.Yolo != nil {
60+ t.Errorf("jan = %+v", jan)
61+ }
62+}
63+
38 // A name that could not be a command name, or an empty path, is a config64 // A name that could not be a command name, or an empty path, is a config
39 // error naming the culprit; no mmConfigs at all is fine.65 // error naming the culprit; no mmConfigs at all is fine.
40 func TestMMConfigsValidation(t *testing.T) {66 func TestMMConfigsValidation(t *testing.T) {
@@ -47,6 +73,7 @@ func TestMMConfigsValidation(t *testing.T) {
47 {"mmConfigs:\n ask bob: a.yaml\n", `"ask bob" is not a valid name`},73 {"mmConfigs:\n ask bob: a.yaml\n", `"ask bob" is not a valid name`},
48 {"mmConfigs:\n -bob: a.yaml\n", `"-bob" is not a valid name`},74 {"mmConfigs:\n -bob: a.yaml\n", `"-bob" is not a valid name`},
49 {"mmConfigs:\n bob: \"\"\n", "mmConfigs.bob: the path must not be empty"},75 {"mmConfigs:\n bob: \"\"\n", "mmConfigs.bob: the path must not be empty"},
76+ {"mmConfigs:\n bob:\n description: no path\n", "mmConfigs.bob: the path must not be empty"},
50 {"mmConfigs:\n with-context-bob: a.yaml\n", "it would read as /ask-with-context"},77 {"mmConfigs:\n with-context-bob: a.yaml\n", "it would read as /ask-with-context"},
51 } {78 } {
52 Cfg = defaults79 Cfg = defaults
modified internal/config/config.go +6 -5
@@ -174,11 +174,12 @@ type Config struct {
174174 // `yolo: false`; any value adds a banner warning (Deprecations).
175175 Permissions string `yaml:"permissions"`
176176
177- // MMConfigs: the other mm agents this one can ask (`/ask-<name>`), name
178- // -> path of that agent's YAML file. A relative path is relative to THIS
179- // file's directory, and Load makes it absolute (agents.go). Each name
180- // becomes a command name, hence its format.
181- MMConfigs map[string]string `yaml:"mmConfigs"`
177+ // MMConfigs: the other mm agents this one can ask (`/ask-<name>`, and
178+ // the `ask_agent` tool), name -> AgentRef: the path of that agent's YAML
179+ // file, written alone or with a description and a yolo. A relative path
180+ // is relative to THIS file's directory, and Load makes it absolute
181+ // (agents.go). Each name becomes a command name, hence its format.
182+ MMConfigs map[string]AgentRef `yaml:"mmConfigs"`
182183 }
183184
184185 // YoloEnv names the variable that overrides `yolo` for one run ("true",
@@ -174,11 +174,12 @@ type Config struct {
174 // `yolo: false`; any value adds a banner warning (Deprecations).174 // `yolo: false`; any value adds a banner warning (Deprecations).
175 Permissions string `yaml:"permissions"`175 Permissions string `yaml:"permissions"`
176 176
177- // MMConfigs: the other mm agents this one can ask (`/ask-<name>`), name177+ // MMConfigs: the other mm agents this one can ask (`/ask-<name>`, and
178- // -> path of that agent's YAML file. A relative path is relative to THIS178+ // the `ask_agent` tool), name -> AgentRef: the path of that agent's YAML
179- // file's directory, and Load makes it absolute (agents.go). Each name179+ // file, written alone or with a description and a yolo. A relative path
180- // becomes a command name, hence its format.180+ // is relative to THIS file's directory, and Load makes it absolute
181- MMConfigs map[string]string `yaml:"mmConfigs"`181+ // (agents.go). Each name becomes a command name, hence its format.
182+ MMConfigs map[string]AgentRef `yaml:"mmConfigs"`
182 }183 }
183 184
184 // YoloEnv names the variable that overrides `yolo` for one run ("true",185 // YoloEnv names the variable that overrides `yolo` for one run ("true",
added internal/engine/conversation.go +45 -0
new file mode 100644
@@ -0,0 +1,45 @@
1+package engine
2+
3+import (
4+ "context"
5+
6+ "github.com/firebase/genkit/go/ai"
7+)
8+
9+// conversationKey is the context key of the messages a generation started
10+// from.
11+type conversationKey struct{}
12+
13+// WithConversation marks ctx with the messages Generate was given, so that a
14+// tool running in that generation can read them — `ask_agent` with
15+// with_context sends them to the agent it asks.
16+func WithConversation(ctx context.Context, messages []*ai.Message) context.Context {
17+ return context.WithValue(ctx, conversationKey{}, messages)
18+}
19+
20+// Conversation returns the messages of the generation ctx belongs to: the
21+// history as it stood when the turn started (this turn's tool calls are not
22+// in it). nil outside a generation.
23+func Conversation(ctx context.Context) []*ai.Message {
24+ messages, _ := ctx.Value(conversationKey{}).([]*ai.Message)
25+ return messages
26+}
27+
28+// heartbeatKey is the context key of the generation's watchdog beat.
29+type heartbeatKey struct{}
30+
31+// withHeartbeat marks ctx with the beat of the generation's watchdog.
32+func withHeartbeat(ctx context.Context, beat func()) context.Context {
33+ return context.WithValue(ctx, heartbeatKey{}, beat)
34+}
35+
36+// Heartbeat returns what keeps the generation's watchdog from firing — a
37+// tool whose work streams signs of life (ask_agent: the asked agent's text,
38+// thoughts and tool calls) calls it on each one; the watchdog otherwise
39+// keeps counting while a tool runs. A no-op outside a generation.
40+func Heartbeat(ctx context.Context) func() {
41+ if beat, ok := ctx.Value(heartbeatKey{}).(func()); ok {
42+ return beat
43+ }
44+ return func() {}
45+}
new file mode 100644
@@ -0,0 +1,45 @@
1+package engine
2+
3+import (
4+ "context"
5+
6+ "github.com/firebase/genkit/go/ai"
7+)
8+
9+// conversationKey is the context key of the messages a generation started
10+// from.
11+type conversationKey struct{}
12+
13+// WithConversation marks ctx with the messages Generate was given, so that a
14+// tool running in that generation can read them — `ask_agent` with
15+// with_context sends them to the agent it asks.
16+func WithConversation(ctx context.Context, messages []*ai.Message) context.Context {
17+ return context.WithValue(ctx, conversationKey{}, messages)
18+}
19+
20+// Conversation returns the messages of the generation ctx belongs to: the
21+// history as it stood when the turn started (this turn's tool calls are not
22+// in it). nil outside a generation.
23+func Conversation(ctx context.Context) []*ai.Message {
24+ messages, _ := ctx.Value(conversationKey{}).([]*ai.Message)
25+ return messages
26+}
27+
28+// heartbeatKey is the context key of the generation's watchdog beat.
29+type heartbeatKey struct{}
30+
31+// withHeartbeat marks ctx with the beat of the generation's watchdog.
32+func withHeartbeat(ctx context.Context, beat func()) context.Context {
33+ return context.WithValue(ctx, heartbeatKey{}, beat)
34+}
35+
36+// Heartbeat returns what keeps the generation's watchdog from firing — a
37+// tool whose work streams signs of life (ask_agent: the asked agent's text,
38+// thoughts and tool calls) calls it on each one; the watchdog otherwise
39+// keeps counting while a tool runs. A no-op outside a generation.
40+func Heartbeat(ctx context.Context) func() {
41+ if beat, ok := ctx.Value(heartbeatKey{}).(func()); ok {
42+ return beat
43+ }
44+ return func() {}
45+}
modified internal/engine/engine.go +2 -0
@@ -241,6 +241,7 @@ func squeeze(s string) string {
241241 // is hung and we cancel the context. This is the "silent loop" of
242242 // STALL_DETECTION_PROBLEM.md — a liveness failure, not a behavioural one.
243243 func (e *Engine) Generate(ctx context.Context, messages []*ai.Message, tools []ai.ToolRef) (*ai.ModelResponse, []*ai.Message, error) {
244+ ctx = WithConversation(ctx, messages)
244245 // Genkit only hands us back the LAST message. The intermediate turns — "I
245246 // am calling bash", "here is the output" — exist only in the request it
246247 // builds for the next turn. This middleware captures that request on its way
@@ -409,6 +410,7 @@ func (e *Engine) Generate(ctx context.Context, messages []*ai.Message, tools []a
409410 defer cancelGen()
410411 pl = p
411412 beat := pl.beat
413+ genCtx = withHeartbeat(genCtx, beat)
412414
413415 // A thinking model streams its reasoning in a field Genkit drops, so the
414416 // streaming callback below never runs while it thinks (reasoning.go). The
@@ -241,6 +241,7 @@ func squeeze(s string) string {
241 // is hung and we cancel the context. This is the "silent loop" of241 // is hung and we cancel the context. This is the "silent loop" of
242 // STALL_DETECTION_PROBLEM.md — a liveness failure, not a behavioural one.242 // STALL_DETECTION_PROBLEM.md — a liveness failure, not a behavioural one.
243 func (e *Engine) Generate(ctx context.Context, messages []*ai.Message, tools []ai.ToolRef) (*ai.ModelResponse, []*ai.Message, error) {243 func (e *Engine) Generate(ctx context.Context, messages []*ai.Message, tools []ai.ToolRef) (*ai.ModelResponse, []*ai.Message, error) {
244+ ctx = WithConversation(ctx, messages)
244 // Genkit only hands us back the LAST message. The intermediate turns — "I245 // Genkit only hands us back the LAST message. The intermediate turns — "I
245 // am calling bash", "here is the output" — exist only in the request it246 // am calling bash", "here is the output" — exist only in the request it
246 // builds for the next turn. This middleware captures that request on its way247 // builds for the next turn. This middleware captures that request on its way
@@ -409,6 +410,7 @@ func (e *Engine) Generate(ctx context.Context, messages []*ai.Message, tools []a
409 defer cancelGen()410 defer cancelGen()
410 pl = p411 pl = p
411 beat := pl.beat412 beat := pl.beat
413+ genCtx = withHeartbeat(genCtx, beat)
412 414
413 // A thinking model streams its reasoning in a field Genkit drops, so the415 // A thinking model streams its reasoning in a field Genkit drops, so the
414 // streaming callback below never runs while it thinks (reasoning.go). The416 // streaming callback below never runs while it thinks (reasoning.go). The
modified internal/engine/engine_test.go +62 -0
@@ -15,6 +15,7 @@ package engine
1515 import (
1616 "context"
1717 "testing"
18+ "time"
1819
1920 "mm/internal/config"
2021 "mm/internal/detector"
@@ -413,3 +414,64 @@ func TestStuckToolEndsTheTurnAndIsExplained(t *testing.T) {
413414 t.Errorf("Explain = %q\nwant %q", got, want)
414415 }
415416 }
417+
418+// A tool running in a generation reads the conversation the turn started
419+// from: what ask_agent's with_context sends.
420+func TestToolsSeeTheConversation(t *testing.T) {
421+ call := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "peek", Input: map[string]any{}}))
422+ g, _ := fakeGenkit(t, call, modelTurn(ai.NewTextPart("done")))
423+ var seen []*ai.Message
424+ genkit.DefineTool(g, "peek", "look at the conversation",
425+ func(tctx *ai.ToolContext, _ struct{}) (string, error) {
426+ seen = Conversation(tctx)
427+ return "ok", nil
428+ })
429+ e := testEngine(g)
430+ e.Provider, e.Backend = registry["dmr"], Backend{Provider: "dmr"}
431+ question := ai.NewUserTextMessage("what changed?")
432+ if _, _, err := e.Generate(context.Background(), []*ai.Message{question}, []ai.ToolRef{ai.ToolName("peek")}); err != nil {
433+ t.Fatal(err)
434+ }
435+ if len(seen) != 1 || seen[0].Text() != "what changed?" {
436+ t.Errorf("the tool saw %d message(s): %v", len(seen), seen)
437+ }
438+}
439+
440+// A tool that beats the heartbeat keeps the turn alive past watchdogTimeout —
441+// ask_agent does it on every event of the agent it asked; a tool that does
442+// not is still cut (ticket 57).
443+func TestHeartbeatKeepsALongToolAlive(t *testing.T) {
444+ saved, savedFirst := config.Cfg.WatchdogTimeout, config.Cfg.FirstTokenTimeout
445+ config.Cfg.WatchdogTimeout, config.Cfg.FirstTokenTimeout = time.Second, time.Second
446+ t.Cleanup(func() { config.Cfg.WatchdogTimeout, config.Cfg.FirstTokenTimeout = saved, savedFirst })
447+
448+ run := func(beats bool) error {
449+ call := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "slow", Input: map[string]any{}}))
450+ g, _ := fakeGenkit(t, call, modelTurn(ai.NewTextPart("done")))
451+ genkit.DefineTool(g, "slow", "works for a while",
452+ func(tctx *ai.ToolContext, _ struct{}) (string, error) {
453+ beat := Heartbeat(tctx)
454+ for range 14 {
455+ select {
456+ case <-tctx.Done():
457+ return "", tctx.Err()
458+ case <-time.After(200 * time.Millisecond):
459+ }
460+ if beats {
461+ beat()
462+ }
463+ }
464+ return "ok", nil
465+ })
466+ e := testEngine(g)
467+ e.Provider, e.Backend = registry["dmr"], Backend{Provider: "dmr"}
468+ _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("go")}, []ai.ToolRef{ai.ToolName("slow")})
469+ return err
470+ }
471+ if err := run(true); err != nil {
472+ t.Errorf("beating tool: %v", err)
473+ }
474+ if err := run(false); err == nil {
475+ t.Error("a silent 2.8s tool survived a 1s watchdog")
476+ }
477+}
@@ -15,6 +15,7 @@ package engine
15 import (15 import (
16 "context"16 "context"
17 "testing"17 "testing"
18+ "time"
18 19
19 "mm/internal/config"20 "mm/internal/config"
20 "mm/internal/detector"21 "mm/internal/detector"
@@ -413,3 +414,64 @@ func TestStuckToolEndsTheTurnAndIsExplained(t *testing.T) {
413 t.Errorf("Explain = %q\nwant %q", got, want)414 t.Errorf("Explain = %q\nwant %q", got, want)
414 }415 }
415 }416 }
417+
418+// A tool running in a generation reads the conversation the turn started
419+// from: what ask_agent's with_context sends.
420+func TestToolsSeeTheConversation(t *testing.T) {
421+ call := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "peek", Input: map[string]any{}}))
422+ g, _ := fakeGenkit(t, call, modelTurn(ai.NewTextPart("done")))
423+ var seen []*ai.Message
424+ genkit.DefineTool(g, "peek", "look at the conversation",
425+ func(tctx *ai.ToolContext, _ struct{}) (string, error) {
426+ seen = Conversation(tctx)
427+ return "ok", nil
428+ })
429+ e := testEngine(g)
430+ e.Provider, e.Backend = registry["dmr"], Backend{Provider: "dmr"}
431+ question := ai.NewUserTextMessage("what changed?")
432+ if _, _, err := e.Generate(context.Background(), []*ai.Message{question}, []ai.ToolRef{ai.ToolName("peek")}); err != nil {
433+ t.Fatal(err)
434+ }
435+ if len(seen) != 1 || seen[0].Text() != "what changed?" {
436+ t.Errorf("the tool saw %d message(s): %v", len(seen), seen)
437+ }
438+}
439+
440+// A tool that beats the heartbeat keeps the turn alive past watchdogTimeout —
441+// ask_agent does it on every event of the agent it asked; a tool that does
442+// not is still cut (ticket 57).
443+func TestHeartbeatKeepsALongToolAlive(t *testing.T) {
444+ saved, savedFirst := config.Cfg.WatchdogTimeout, config.Cfg.FirstTokenTimeout
445+ config.Cfg.WatchdogTimeout, config.Cfg.FirstTokenTimeout = time.Second, time.Second
446+ t.Cleanup(func() { config.Cfg.WatchdogTimeout, config.Cfg.FirstTokenTimeout = saved, savedFirst })
447+
448+ run := func(beats bool) error {
449+ call := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "slow", Input: map[string]any{}}))
450+ g, _ := fakeGenkit(t, call, modelTurn(ai.NewTextPart("done")))
451+ genkit.DefineTool(g, "slow", "works for a while",
452+ func(tctx *ai.ToolContext, _ struct{}) (string, error) {
453+ beat := Heartbeat(tctx)
454+ for range 14 {
455+ select {
456+ case <-tctx.Done():
457+ return "", tctx.Err()
458+ case <-time.After(200 * time.Millisecond):
459+ }
460+ if beats {
461+ beat()
462+ }
463+ }
464+ return "ok", nil
465+ })
466+ e := testEngine(g)
467+ e.Provider, e.Backend = registry["dmr"], Backend{Provider: "dmr"}
468+ _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("go")}, []ai.ToolRef{ai.ToolName("slow")})
469+ return err
470+ }
471+ if err := run(true); err != nil {
472+ t.Errorf("beating tool: %v", err)
473+ }
474+ if err := run(false); err == nil {
475+ t.Error("a silent 2.8s tool survived a 1s watchdog")
476+ }
477+}
added internal/tools/askagent.go +205 -0
new file mode 100644
@@ -0,0 +1,205 @@
1+package tools
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "strings"
8+
9+ "mm/internal/ask"
10+ "mm/internal/config"
11+ "mm/internal/consent"
12+ "mm/internal/detector"
13+ "mm/internal/engine"
14+ "mm/internal/spinner"
15+ "mm/internal/ui"
16+
17+ "github.com/firebase/genkit/go/ai"
18+ "github.com/firebase/genkit/go/genkit"
19+)
20+
21+// --- ask_agent ---------------------------------------------------------------
22+//
23+// Hands a task to one of the agents of mmConfigs and returns its answer as
24+// the tool's result: the model decides when, from the agents' descriptions,
25+// like it decides when to run a command. The asked agent runs through the
26+// same pool as /ask, so its text, thoughts and tool calls stream through this
27+// turn's sink — the user watches it work — and its calls are decided by its
28+// own `yolo` in mmConfigs, else by this agent's.
29+//
30+// The call itself is gated like bash: with yolo off, handing work to another
31+// agent is something the user approves.
32+
33+type askAgentInput struct {
34+ Agent string `json:"agent" jsonschema_description:"Name of the agent to ask, one of those listed in this tool's description."`
35+ Prompt string `json:"prompt" jsonschema_description:"The task or question for that agent, complete and self-contained: it does not see this conversation unless with_context is true. Name the files it must read or write."`
36+ WithContext bool `json:"with_context,omitempty" jsonschema_description:"Optional. true sends this conversation (questions, answers, the commands run) along with the prompt."`
37+}
38+
39+// AgentAsker is what ask_agent needs from ask.Pool; tests give it a fake.
40+type AgentAsker interface {
41+ Ask(ctx context.Context, name, configPath, cwd, prompt string, sink ui.Sink) (string, error)
42+}
43+
44+// AskAgent declares the `ask_agent` tool over the agents of mmConfigs. The
45+// caller declares it only when there is at least one.
46+//
47+// if len(config.Cfg.MMConfigs) > 0 {
48+// tools = append(tools, tools.AskAgent(g, d, pool))
49+// }
50+func AskAgent(g *genkit.Genkit, d *detector.LoopDetector, asker AgentAsker) ai.ToolRef {
51+ return genkit.DefineTool(g, "ask_agent", askAgentDescription(),
52+ func(tctx *ai.ToolContext, in askAgentInput) (string, error) {
53+ return askAgentCall{asker: asker, detector: d}.run(tctx, in)
54+ })
55+}
56+
57+// askAgentDescription lists the agents, each with what its mmConfigs entry
58+// says it is for — the model's only basis for choosing one.
59+func askAgentDescription() string {
60+ var b strings.Builder
61+ b.WriteString(`Hand a task or a question to another agent and get its answer back as this tool's result. Each agent is a separate assistant with its own model, tools and skills, working in the same project directory. Use one when the request matches what it is for — below — instead of doing that work yourself. Give it a complete, self-contained prompt: it does not see this conversation unless with_context is true. It may take minutes. Its answer is text: read it, then carry on — report it, check it, or hand the next step to another agent.
62+Agents:`)
63+ for _, name := range config.AgentNames() {
64+ fmt.Fprintf(&b, "\n %s — %s", name, agentPurpose(name))
65+ }
66+ return b.String()
67+}
68+
69+// agentPurpose is an agent's description in mmConfigs, else its acp.title.
70+func agentPurpose(name string) string {
71+ ref := config.Cfg.MMConfigs[name]
72+ if d := strings.TrimSpace(ref.Description); d != "" {
73+ return d
74+ }
75+ if title := ask.Identify(ref.Path).Title; title != "" && title != config.DefaultACPTitle {
76+ return title
77+ }
78+ return "(no description)"
79+}
80+
81+// askAgentCall is what one call of the tool works with.
82+type askAgentCall struct {
83+ asker AgentAsker
84+ detector *detector.LoopDetector
85+}
86+
87+// run checks the call, then asks: through the turn's sink in the window and
88+// the editor, through a LineSink in the line mode.
89+func (call askAgentCall) run(ctx context.Context, in askAgentInput) (string, error) {
90+ key := in.Agent + ": " + in.Prompt
91+ ref, ok := config.Cfg.MMConfigs[in.Agent]
92+ if !ok {
93+ return record(call.detector, "ask_agent", key, fmt.Sprintf("[ask_agent: unknown agent %q — agents: %s]", in.Agent, strings.Join(config.AgentNames(), ", ")))
94+ }
95+ if strings.TrimSpace(in.Prompt) == "" {
96+ return record(call.detector, "ask_agent", key, "[ask_agent: the prompt is empty — say what "+in.Agent+" must do]")
97+ }
98+ transcript := ""
99+ if in.WithContext {
100+ transcript = ask.Transcript(engine.Conversation(ctx), config.Cfg.MaxOutput)
101+ }
102+ prompt := ask.Prompt(in.Prompt, transcript)
103+ id := ask.Identify(ref.Path)
104+ title := fmt.Sprintf("ask_agent: %s (%s)", in.Agent, ask.Describe(ref.Path))
105+ if s := ui.ActiveSink(); s != nil {
106+ return call.evented(ctx, s, in, ref.Path, prompt, title, id)
107+ }
108+
109+ announce("🗣 " + title + " — " + firstLine(in.Prompt))
110+ if !confirmed(ctx, "ask_agent", title) {
111+ return refuse(call.detector, "ask_agent", key, "[ask_agent rejected by the user]")
112+ }
113+ spinner.Stop()
114+ cwd, _ := os.Getwd()
115+ answer, err := call.asker.Ask(ctx, in.Agent, ref.Path, cwd, prompt, beating(ctx, &ask.LineSink{Cwd: cwd}))
116+ fmt.Println()
117+ spinner.Start("Thinking")
118+ return record(call.detector, "ask_agent", key, askAgentResult(in.Agent, answer, err))
119+}
120+
121+// evented is ask_agent for an event-consuming front end (the window, an
122+// editor): one tool call for the hand-off, the asked agent's own events
123+// inside it, and — for a sink that is a ui.Speaker — the asked agent named
124+// while it answers.
125+func (call askAgentCall) evented(ctx context.Context, s ui.Sink, in askAgentInput, path, prompt, title string, id ask.Identity) (string, error) {
126+ key := in.Agent + ": " + in.Prompt
127+ ev := ui.ToolEvent{
128+ ID: ui.NextCallID(),
129+ Tool: "ask_agent",
130+ Title: title,
131+ Kind: "other",
132+ Input: map[string]any{"agent": in.Agent, "prompt": in.Prompt, "with_context": in.WithContext},
133+ }
134+ s.ToolStart(ev)
135+ if !consent.Gate(ctx, s, ev) {
136+ out := "[ask_agent rejected by the user]"
137+ s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true})
138+ return record(call.detector, "ask_agent", key, out)
139+ }
140+ s.ToolRunning(ev.ID)
141+ if sp, ok := s.(ui.Speaker); ok {
142+ sp.Speaking(in.Agent, id.Title, id.Model, id.Provider)
143+ }
144+ answer, err := call.asker.Ask(ctx, in.Agent, path, s.WorkDir(), prompt, beating(ctx, s))
145+ if sp, ok := s.(ui.Speaker); ok {
146+ sp.Spoken()
147+ }
148+ out := askAgentResult(in.Agent, answer, err)
149+ // The answer has streamed already: the call's own output only says how
150+ // it ended, not the answer a second time.
151+ summary := fmt.Sprintf("%s answered (%d characters)", in.Agent, len([]rune(answer)))
152+ if err != nil || strings.TrimSpace(answer) == "" {
153+ summary = out
154+ }
155+ s.ToolEnd(ev.ID, ui.ToolResult{Output: summary, Failed: err != nil})
156+ return record(call.detector, "ask_agent", key, out)
157+}
158+
159+// beatingSink passes every event on, and beats this generation's watchdog on
160+// each: the asked agent may work for many minutes inside one tool call, and
161+// what it streams is proof that it is alive. When it goes silent, its own
162+// watchdog is the one that fires. A permission question it asks is not a
163+// sign of life until it is answered.
164+type beatingSink struct {
165+ ui.Sink
166+ beat func()
167+}
168+
169+func beating(ctx context.Context, s ui.Sink) ui.Sink {
170+ return beatingSink{Sink: s, beat: engine.Heartbeat(ctx)}
171+}
172+
173+func (b beatingSink) Text(s string) { b.beat(); b.Sink.Text(s) }
174+func (b beatingSink) Reasoning(s string) { b.beat(); b.Sink.Reasoning(s) }
175+func (b beatingSink) ToolStart(ev ui.ToolEvent) { b.beat(); b.Sink.ToolStart(ev) }
176+func (b beatingSink) ToolRunning(id string) { b.beat(); b.Sink.ToolRunning(id) }
177+func (b beatingSink) ToolEnd(id string, r ui.ToolResult) {
178+ b.beat()
179+ b.Sink.ToolEnd(id, r)
180+}
181+func (b beatingSink) Allow(ctx context.Context, ev ui.ToolEvent) bool {
182+ ok := b.Sink.Allow(ctx, ev)
183+ b.beat()
184+ return ok
185+}
186+
187+// askAgentResult is what the model reads: the answer (capped like any tool
188+// output), or why there is none.
189+func askAgentResult(agent, answer string, err error) string {
190+ switch {
191+ case err != nil && answer != "":
192+ return truncate(answer) + fmt.Sprintf("\n\n[ask_agent: %s stopped before finishing: %v]", agent, err)
193+ case err != nil:
194+ return fmt.Sprintf("[ask_agent: %s failed: %v]", agent, err)
195+ case strings.TrimSpace(answer) == "":
196+ return fmt.Sprintf("[ask_agent: %s gave no answer]", agent)
197+ }
198+ return truncate(answer)
199+}
200+
201+// firstLine is the start of a prompt, for the line mode's one-line announce.
202+func firstLine(s string) string {
203+ line, _, _ := strings.Cut(strings.TrimSpace(s), "\n")
204+ return line
205+}
new file mode 100644
@@ -0,0 +1,205 @@
1+package tools
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "strings"
8+
9+ "mm/internal/ask"
10+ "mm/internal/config"
11+ "mm/internal/consent"
12+ "mm/internal/detector"
13+ "mm/internal/engine"
14+ "mm/internal/spinner"
15+ "mm/internal/ui"
16+
17+ "github.com/firebase/genkit/go/ai"
18+ "github.com/firebase/genkit/go/genkit"
19+)
20+
21+// --- ask_agent ---------------------------------------------------------------
22+//
23+// Hands a task to one of the agents of mmConfigs and returns its answer as
24+// the tool's result: the model decides when, from the agents' descriptions,
25+// like it decides when to run a command. The asked agent runs through the
26+// same pool as /ask, so its text, thoughts and tool calls stream through this
27+// turn's sink — the user watches it work — and its calls are decided by its
28+// own `yolo` in mmConfigs, else by this agent's.
29+//
30+// The call itself is gated like bash: with yolo off, handing work to another
31+// agent is something the user approves.
32+
33+type askAgentInput struct {
34+ Agent string `json:"agent" jsonschema_description:"Name of the agent to ask, one of those listed in this tool's description."`
35+ Prompt string `json:"prompt" jsonschema_description:"The task or question for that agent, complete and self-contained: it does not see this conversation unless with_context is true. Name the files it must read or write."`
36+ WithContext bool `json:"with_context,omitempty" jsonschema_description:"Optional. true sends this conversation (questions, answers, the commands run) along with the prompt."`
37+}
38+
39+// AgentAsker is what ask_agent needs from ask.Pool; tests give it a fake.
40+type AgentAsker interface {
41+ Ask(ctx context.Context, name, configPath, cwd, prompt string, sink ui.Sink) (string, error)
42+}
43+
44+// AskAgent declares the `ask_agent` tool over the agents of mmConfigs. The
45+// caller declares it only when there is at least one.
46+//
47+// if len(config.Cfg.MMConfigs) > 0 {
48+// tools = append(tools, tools.AskAgent(g, d, pool))
49+// }
50+func AskAgent(g *genkit.Genkit, d *detector.LoopDetector, asker AgentAsker) ai.ToolRef {
51+ return genkit.DefineTool(g, "ask_agent", askAgentDescription(),
52+ func(tctx *ai.ToolContext, in askAgentInput) (string, error) {
53+ return askAgentCall{asker: asker, detector: d}.run(tctx, in)
54+ })
55+}
56+
57+// askAgentDescription lists the agents, each with what its mmConfigs entry
58+// says it is for — the model's only basis for choosing one.
59+func askAgentDescription() string {
60+ var b strings.Builder
61+ b.WriteString(`Hand a task or a question to another agent and get its answer back as this tool's result. Each agent is a separate assistant with its own model, tools and skills, working in the same project directory. Use one when the request matches what it is for — below — instead of doing that work yourself. Give it a complete, self-contained prompt: it does not see this conversation unless with_context is true. It may take minutes. Its answer is text: read it, then carry on — report it, check it, or hand the next step to another agent.
62+Agents:`)
63+ for _, name := range config.AgentNames() {
64+ fmt.Fprintf(&b, "\n %s — %s", name, agentPurpose(name))
65+ }
66+ return b.String()
67+}
68+
69+// agentPurpose is an agent's description in mmConfigs, else its acp.title.
70+func agentPurpose(name string) string {
71+ ref := config.Cfg.MMConfigs[name]
72+ if d := strings.TrimSpace(ref.Description); d != "" {
73+ return d
74+ }
75+ if title := ask.Identify(ref.Path).Title; title != "" && title != config.DefaultACPTitle {
76+ return title
77+ }
78+ return "(no description)"
79+}
80+
81+// askAgentCall is what one call of the tool works with.
82+type askAgentCall struct {
83+ asker AgentAsker
84+ detector *detector.LoopDetector
85+}
86+
87+// run checks the call, then asks: through the turn's sink in the window and
88+// the editor, through a LineSink in the line mode.
89+func (call askAgentCall) run(ctx context.Context, in askAgentInput) (string, error) {
90+ key := in.Agent + ": " + in.Prompt
91+ ref, ok := config.Cfg.MMConfigs[in.Agent]
92+ if !ok {
93+ return record(call.detector, "ask_agent", key, fmt.Sprintf("[ask_agent: unknown agent %q — agents: %s]", in.Agent, strings.Join(config.AgentNames(), ", ")))
94+ }
95+ if strings.TrimSpace(in.Prompt) == "" {
96+ return record(call.detector, "ask_agent", key, "[ask_agent: the prompt is empty — say what "+in.Agent+" must do]")
97+ }
98+ transcript := ""
99+ if in.WithContext {
100+ transcript = ask.Transcript(engine.Conversation(ctx), config.Cfg.MaxOutput)
101+ }
102+ prompt := ask.Prompt(in.Prompt, transcript)
103+ id := ask.Identify(ref.Path)
104+ title := fmt.Sprintf("ask_agent: %s (%s)", in.Agent, ask.Describe(ref.Path))
105+ if s := ui.ActiveSink(); s != nil {
106+ return call.evented(ctx, s, in, ref.Path, prompt, title, id)
107+ }
108+
109+ announce("🗣 " + title + " — " + firstLine(in.Prompt))
110+ if !confirmed(ctx, "ask_agent", title) {
111+ return refuse(call.detector, "ask_agent", key, "[ask_agent rejected by the user]")
112+ }
113+ spinner.Stop()
114+ cwd, _ := os.Getwd()
115+ answer, err := call.asker.Ask(ctx, in.Agent, ref.Path, cwd, prompt, beating(ctx, &ask.LineSink{Cwd: cwd}))
116+ fmt.Println()
117+ spinner.Start("Thinking")
118+ return record(call.detector, "ask_agent", key, askAgentResult(in.Agent, answer, err))
119+}
120+
121+// evented is ask_agent for an event-consuming front end (the window, an
122+// editor): one tool call for the hand-off, the asked agent's own events
123+// inside it, and — for a sink that is a ui.Speaker — the asked agent named
124+// while it answers.
125+func (call askAgentCall) evented(ctx context.Context, s ui.Sink, in askAgentInput, path, prompt, title string, id ask.Identity) (string, error) {
126+ key := in.Agent + ": " + in.Prompt
127+ ev := ui.ToolEvent{
128+ ID: ui.NextCallID(),
129+ Tool: "ask_agent",
130+ Title: title,
131+ Kind: "other",
132+ Input: map[string]any{"agent": in.Agent, "prompt": in.Prompt, "with_context": in.WithContext},
133+ }
134+ s.ToolStart(ev)
135+ if !consent.Gate(ctx, s, ev) {
136+ out := "[ask_agent rejected by the user]"
137+ s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true})
138+ return record(call.detector, "ask_agent", key, out)
139+ }
140+ s.ToolRunning(ev.ID)
141+ if sp, ok := s.(ui.Speaker); ok {
142+ sp.Speaking(in.Agent, id.Title, id.Model, id.Provider)
143+ }
144+ answer, err := call.asker.Ask(ctx, in.Agent, path, s.WorkDir(), prompt, beating(ctx, s))
145+ if sp, ok := s.(ui.Speaker); ok {
146+ sp.Spoken()
147+ }
148+ out := askAgentResult(in.Agent, answer, err)
149+ // The answer has streamed already: the call's own output only says how
150+ // it ended, not the answer a second time.
151+ summary := fmt.Sprintf("%s answered (%d characters)", in.Agent, len([]rune(answer)))
152+ if err != nil || strings.TrimSpace(answer) == "" {
153+ summary = out
154+ }
155+ s.ToolEnd(ev.ID, ui.ToolResult{Output: summary, Failed: err != nil})
156+ return record(call.detector, "ask_agent", key, out)
157+}
158+
159+// beatingSink passes every event on, and beats this generation's watchdog on
160+// each: the asked agent may work for many minutes inside one tool call, and
161+// what it streams is proof that it is alive. When it goes silent, its own
162+// watchdog is the one that fires. A permission question it asks is not a
163+// sign of life until it is answered.
164+type beatingSink struct {
165+ ui.Sink
166+ beat func()
167+}
168+
169+func beating(ctx context.Context, s ui.Sink) ui.Sink {
170+ return beatingSink{Sink: s, beat: engine.Heartbeat(ctx)}
171+}
172+
173+func (b beatingSink) Text(s string) { b.beat(); b.Sink.Text(s) }
174+func (b beatingSink) Reasoning(s string) { b.beat(); b.Sink.Reasoning(s) }
175+func (b beatingSink) ToolStart(ev ui.ToolEvent) { b.beat(); b.Sink.ToolStart(ev) }
176+func (b beatingSink) ToolRunning(id string) { b.beat(); b.Sink.ToolRunning(id) }
177+func (b beatingSink) ToolEnd(id string, r ui.ToolResult) {
178+ b.beat()
179+ b.Sink.ToolEnd(id, r)
180+}
181+func (b beatingSink) Allow(ctx context.Context, ev ui.ToolEvent) bool {
182+ ok := b.Sink.Allow(ctx, ev)
183+ b.beat()
184+ return ok
185+}
186+
187+// askAgentResult is what the model reads: the answer (capped like any tool
188+// output), or why there is none.
189+func askAgentResult(agent, answer string, err error) string {
190+ switch {
191+ case err != nil && answer != "":
192+ return truncate(answer) + fmt.Sprintf("\n\n[ask_agent: %s stopped before finishing: %v]", agent, err)
193+ case err != nil:
194+ return fmt.Sprintf("[ask_agent: %s failed: %v]", agent, err)
195+ case strings.TrimSpace(answer) == "":
196+ return fmt.Sprintf("[ask_agent: %s gave no answer]", agent)
197+ }
198+ return truncate(answer)
199+}
200+
201+// firstLine is the start of a prompt, for the line mode's one-line announce.
202+func firstLine(s string) string {
203+ line, _, _ := strings.Cut(strings.TrimSpace(s), "\n")
204+ return line
205+}
added internal/tools/askagent_test.go +166 -0
new file mode 100644
@@ -0,0 +1,166 @@
1+package tools
2+
3+import (
4+ "context"
5+ "errors"
6+ "strings"
7+ "testing"
8+
9+ "mm/internal/ask"
10+ "mm/internal/config"
11+ "mm/internal/detector"
12+ "mm/internal/engine"
13+ "mm/internal/ui"
14+
15+ "github.com/firebase/genkit/go/ai"
16+)
17+
18+// fakeAgentAsker plays the asked agent: it records what it was given and
19+// streams its answer through the sink.
20+type fakeAgentAsker struct {
21+ answer string
22+ err error
23+ got struct{ name, path, cwd, prompt string }
24+ calls int
25+}
26+
27+func (a *fakeAgentAsker) Ask(_ context.Context, name, path, cwd, prompt string, sink ui.Sink) (string, error) {
28+ a.calls++
29+ a.got.name, a.got.path, a.got.cwd, a.got.prompt = name, path, cwd, prompt
30+ sink.Text(a.answer)
31+ return a.answer, a.err
32+}
33+
34+// speakingSink is a recordingSink that also records who it was told speaks.
35+type speakingSink struct {
36+ recordingSink
37+ text strings.Builder
38+ spoke []string
39+}
40+
41+func (k *speakingSink) Text(s string) { k.text.WriteString(s) }
42+func (k *speakingSink) Speaking(name, title, model, provider string) {
43+ k.spoke = append(k.spoke, "speaking:"+name+"|"+title+"|"+provider+"/"+model)
44+}
45+func (k *speakingSink) Spoken() { k.spoke = append(k.spoke, "spoken") }
46+
47+// withAgents sets mmConfigs for one test.
48+func withAgents(t *testing.T, agents map[string]config.AgentRef) {
49+ t.Helper()
50+ saved := config.Cfg.MMConfigs
51+ config.Cfg.MMConfigs = agents
52+ t.Cleanup(func() { config.Cfg.MMConfigs = saved })
53+}
54+
55+func newAskAgentCall(a AgentAsker) askAgentCall {
56+ return askAgentCall{asker: a, detector: detector.NewLoopDetector(10, 3)}
57+}
58+
59+// The description lists every agent with its mmConfigs description, sorted.
60+func TestAskAgentDescriptionListsTheAgents(t *testing.T) {
61+ withAgents(t, map[string]config.AgentRef{
62+ "milo": {Path: "/nowhere/milo.yaml", Description: "Writes specifications."},
63+ "homer": {Path: "/nowhere/homer.yaml"},
64+ })
65+ got := askAgentDescription()
66+ if !strings.Contains(got, "\n homer — (no description)\n milo — Writes specifications.") {
67+ t.Errorf("description:\n%s", got)
68+ }
69+}
70+
71+// In the window or an editor: one tool call for the hand-off, the asked agent
72+// named while it answers, its answer streamed AND returned to the model, the
73+// call's own output a summary rather than the answer twice.
74+func TestAskAgentEvented(t *testing.T) {
75+ yoloOn(t)
76+ withAgents(t, map[string]config.AgentRef{"homer": {Path: "/nowhere/homer.yaml"}})
77+ a := &fakeAgentAsker{answer: "Implemented R1 and R2."}
78+ sink := &speakingSink{recordingSink: recordingSink{dir: "/proj"}}
79+
80+ out, err := newAskAgentCall(a).evented(context.Background(), sink, askAgentInput{Agent: "homer", Prompt: "Implement specs/x.spec.md"}, "/nowhere/homer.yaml", "Implement specs/x.spec.md", "ask_agent: homer (?)", askIdentity("Homer (coder)"))
81+ if err != nil || out != "Implemented R1 and R2." {
82+ t.Fatalf("result %q, err %v", out, err)
83+ }
84+ if a.got.name != "homer" || a.got.cwd != "/proj" || sink.text.String() != out {
85+ t.Errorf("asked %+v, streamed %q", a.got, sink.text.String())
86+ }
87+ if want := []string{"speaking:homer|Homer (coder)|llmman/m", "spoken"}; strings.Join(sink.spoke, ",") != strings.Join(want, ",") {
88+ t.Errorf("speaker events %v, want %v", sink.spoke, want)
89+ }
90+ if sink.started.Tool != "ask_agent" || sink.ended.Output != "homer answered (22 characters)" || sink.ended.Failed {
91+ t.Errorf("tool call: started %+v, ended %+v", sink.started, sink.ended)
92+ }
93+}
94+
95+// With yolo off the hand-off is the user's call: refused, nobody is asked.
96+func TestAskAgentRefused(t *testing.T) {
97+ yoloOff(t)
98+ withAgents(t, map[string]config.AgentRef{"homer": {Path: "/nowhere/homer.yaml"}})
99+ a := &fakeAgentAsker{answer: "never"}
100+ sink := &speakingSink{recordingSink: recordingSink{allow: false}}
101+ out, _ := newAskAgentCall(a).evented(context.Background(), sink, askAgentInput{Agent: "homer", Prompt: "p"}, "x", "p", "t", askIdentity(""))
102+ if a.calls != 0 || out != "[ask_agent rejected by the user]" || sink.asked != 1 || len(sink.spoke) != 0 {
103+ t.Errorf("calls %d, out %q, asked %d, spoke %v", a.calls, out, sink.asked, sink.spoke)
104+ }
105+}
106+
107+// What the model reads when there is no answer: the reason, never an empty
108+// result; an unknown agent or an empty prompt is a usage error naming the fix.
109+func TestAskAgentFailures(t *testing.T) {
110+ if got := askAgentResult("homer", "", errors.New("did not start")); got != "[ask_agent: homer failed: did not start]" {
111+ t.Errorf("failure = %q", got)
112+ }
113+ if got := askAgentResult("homer", " ", nil); got != "[ask_agent: homer gave no answer]" {
114+ t.Errorf("no answer = %q", got)
115+ }
116+ if got := askAgentResult("homer", "half", context.Canceled); !strings.HasPrefix(got, "half\n\n[ask_agent: homer stopped before finishing") {
117+ t.Errorf("partial = %q", got)
118+ }
119+ withAgents(t, map[string]config.AgentRef{"homer": {Path: "x"}, "milo": {Path: "y"}})
120+ call := newAskAgentCall(&fakeAgentAsker{})
121+ if got, _ := call.run(context.Background(), askAgentInput{Agent: "bart", Prompt: "p"}); got != `[ask_agent: unknown agent "bart" — agents: homer, milo]` {
122+ t.Errorf("unknown agent = %q", got)
123+ }
124+ if got, _ := call.run(context.Background(), askAgentInput{Agent: "homer", Prompt: " "}); !strings.Contains(got, "the prompt is empty") {
125+ t.Errorf("empty prompt = %q", got)
126+ }
127+}
128+
129+// with_context sends the conversation the turn started from, before the
130+// prompt; without it the prompt goes alone.
131+func TestAskAgentWithContext(t *testing.T) {
132+ yoloOn(t)
133+ withAgents(t, map[string]config.AgentRef{"milo": {Path: "/nowhere/milo.yaml"}})
134+ ui.SetActive(&speakingSink{})
135+ t.Cleanup(func() { ui.SetActive(nil) })
136+ a := &fakeAgentAsker{answer: "ok"}
137+ ctx := engine.WithConversation(context.Background(), []*ai.Message{
138+ ai.NewUserTextMessage("we use PostgreSQL 16"),
139+ ai.NewModelTextMessage("noted"),
140+ })
141+ if _, err := newAskAgentCall(a).run(ctx, askAgentInput{Agent: "milo", Prompt: "specify the schema", WithContext: true}); err != nil {
142+ t.Fatal(err)
143+ }
144+ if !strings.Contains(a.got.prompt, "PostgreSQL 16") || !strings.HasSuffix(strings.TrimSpace(a.got.prompt), "specify the schema") {
145+ t.Errorf("prompt with context:\n%s", a.got.prompt)
146+ }
147+ if _, err := newAskAgentCall(a).run(ctx, askAgentInput{Agent: "milo", Prompt: "specify the schema"}); err != nil {
148+ t.Fatal(err)
149+ }
150+ if strings.Contains(a.got.prompt, "PostgreSQL") {
151+ t.Errorf("prompt without context carries it:\n%s", a.got.prompt)
152+ }
153+}
154+
155+// yoloOn sets yolo on for one test.
156+func yoloOn(t *testing.T) {
157+ t.Helper()
158+ old := config.Cfg.Yolo
159+ config.Cfg.Yolo = true
160+ t.Cleanup(func() { config.Cfg.Yolo = old })
161+}
162+
163+// askIdentity is an asked agent's identity as its config file would give it.
164+func askIdentity(title string) ask.Identity {
165+ return ask.Identity{Title: title, Model: "m", Provider: "llmman"}
166+}
new file mode 100644
@@ -0,0 +1,166 @@
1+package tools
2+
3+import (
4+ "context"
5+ "errors"
6+ "strings"
7+ "testing"
8+
9+ "mm/internal/ask"
10+ "mm/internal/config"
11+ "mm/internal/detector"
12+ "mm/internal/engine"
13+ "mm/internal/ui"
14+
15+ "github.com/firebase/genkit/go/ai"
16+)
17+
18+// fakeAgentAsker plays the asked agent: it records what it was given and
19+// streams its answer through the sink.
20+type fakeAgentAsker struct {
21+ answer string
22+ err error
23+ got struct{ name, path, cwd, prompt string }
24+ calls int
25+}
26+
27+func (a *fakeAgentAsker) Ask(_ context.Context, name, path, cwd, prompt string, sink ui.Sink) (string, error) {
28+ a.calls++
29+ a.got.name, a.got.path, a.got.cwd, a.got.prompt = name, path, cwd, prompt
30+ sink.Text(a.answer)
31+ return a.answer, a.err
32+}
33+
34+// speakingSink is a recordingSink that also records who it was told speaks.
35+type speakingSink struct {
36+ recordingSink
37+ text strings.Builder
38+ spoke []string
39+}
40+
41+func (k *speakingSink) Text(s string) { k.text.WriteString(s) }
42+func (k *speakingSink) Speaking(name, title, model, provider string) {
43+ k.spoke = append(k.spoke, "speaking:"+name+"|"+title+"|"+provider+"/"+model)
44+}
45+func (k *speakingSink) Spoken() { k.spoke = append(k.spoke, "spoken") }
46+
47+// withAgents sets mmConfigs for one test.
48+func withAgents(t *testing.T, agents map[string]config.AgentRef) {
49+ t.Helper()
50+ saved := config.Cfg.MMConfigs
51+ config.Cfg.MMConfigs = agents
52+ t.Cleanup(func() { config.Cfg.MMConfigs = saved })
53+}
54+
55+func newAskAgentCall(a AgentAsker) askAgentCall {
56+ return askAgentCall{asker: a, detector: detector.NewLoopDetector(10, 3)}
57+}
58+
59+// The description lists every agent with its mmConfigs description, sorted.
60+func TestAskAgentDescriptionListsTheAgents(t *testing.T) {
61+ withAgents(t, map[string]config.AgentRef{
62+ "milo": {Path: "/nowhere/milo.yaml", Description: "Writes specifications."},
63+ "homer": {Path: "/nowhere/homer.yaml"},
64+ })
65+ got := askAgentDescription()
66+ if !strings.Contains(got, "\n homer — (no description)\n milo — Writes specifications.") {
67+ t.Errorf("description:\n%s", got)
68+ }
69+}
70+
71+// In the window or an editor: one tool call for the hand-off, the asked agent
72+// named while it answers, its answer streamed AND returned to the model, the
73+// call's own output a summary rather than the answer twice.
74+func TestAskAgentEvented(t *testing.T) {
75+ yoloOn(t)
76+ withAgents(t, map[string]config.AgentRef{"homer": {Path: "/nowhere/homer.yaml"}})
77+ a := &fakeAgentAsker{answer: "Implemented R1 and R2."}
78+ sink := &speakingSink{recordingSink: recordingSink{dir: "/proj"}}
79+
80+ out, err := newAskAgentCall(a).evented(context.Background(), sink, askAgentInput{Agent: "homer", Prompt: "Implement specs/x.spec.md"}, "/nowhere/homer.yaml", "Implement specs/x.spec.md", "ask_agent: homer (?)", askIdentity("Homer (coder)"))
81+ if err != nil || out != "Implemented R1 and R2." {
82+ t.Fatalf("result %q, err %v", out, err)
83+ }
84+ if a.got.name != "homer" || a.got.cwd != "/proj" || sink.text.String() != out {
85+ t.Errorf("asked %+v, streamed %q", a.got, sink.text.String())
86+ }
87+ if want := []string{"speaking:homer|Homer (coder)|llmman/m", "spoken"}; strings.Join(sink.spoke, ",") != strings.Join(want, ",") {
88+ t.Errorf("speaker events %v, want %v", sink.spoke, want)
89+ }
90+ if sink.started.Tool != "ask_agent" || sink.ended.Output != "homer answered (22 characters)" || sink.ended.Failed {
91+ t.Errorf("tool call: started %+v, ended %+v", sink.started, sink.ended)
92+ }
93+}
94+
95+// With yolo off the hand-off is the user's call: refused, nobody is asked.
96+func TestAskAgentRefused(t *testing.T) {
97+ yoloOff(t)
98+ withAgents(t, map[string]config.AgentRef{"homer": {Path: "/nowhere/homer.yaml"}})
99+ a := &fakeAgentAsker{answer: "never"}
100+ sink := &speakingSink{recordingSink: recordingSink{allow: false}}
101+ out, _ := newAskAgentCall(a).evented(context.Background(), sink, askAgentInput{Agent: "homer", Prompt: "p"}, "x", "p", "t", askIdentity(""))
102+ if a.calls != 0 || out != "[ask_agent rejected by the user]" || sink.asked != 1 || len(sink.spoke) != 0 {
103+ t.Errorf("calls %d, out %q, asked %d, spoke %v", a.calls, out, sink.asked, sink.spoke)
104+ }
105+}
106+
107+// What the model reads when there is no answer: the reason, never an empty
108+// result; an unknown agent or an empty prompt is a usage error naming the fix.
109+func TestAskAgentFailures(t *testing.T) {
110+ if got := askAgentResult("homer", "", errors.New("did not start")); got != "[ask_agent: homer failed: did not start]" {
111+ t.Errorf("failure = %q", got)
112+ }
113+ if got := askAgentResult("homer", " ", nil); got != "[ask_agent: homer gave no answer]" {
114+ t.Errorf("no answer = %q", got)
115+ }
116+ if got := askAgentResult("homer", "half", context.Canceled); !strings.HasPrefix(got, "half\n\n[ask_agent: homer stopped before finishing") {
117+ t.Errorf("partial = %q", got)
118+ }
119+ withAgents(t, map[string]config.AgentRef{"homer": {Path: "x"}, "milo": {Path: "y"}})
120+ call := newAskAgentCall(&fakeAgentAsker{})
121+ if got, _ := call.run(context.Background(), askAgentInput{Agent: "bart", Prompt: "p"}); got != `[ask_agent: unknown agent "bart" — agents: homer, milo]` {
122+ t.Errorf("unknown agent = %q", got)
123+ }
124+ if got, _ := call.run(context.Background(), askAgentInput{Agent: "homer", Prompt: " "}); !strings.Contains(got, "the prompt is empty") {
125+ t.Errorf("empty prompt = %q", got)
126+ }
127+}
128+
129+// with_context sends the conversation the turn started from, before the
130+// prompt; without it the prompt goes alone.
131+func TestAskAgentWithContext(t *testing.T) {
132+ yoloOn(t)
133+ withAgents(t, map[string]config.AgentRef{"milo": {Path: "/nowhere/milo.yaml"}})
134+ ui.SetActive(&speakingSink{})
135+ t.Cleanup(func() { ui.SetActive(nil) })
136+ a := &fakeAgentAsker{answer: "ok"}
137+ ctx := engine.WithConversation(context.Background(), []*ai.Message{
138+ ai.NewUserTextMessage("we use PostgreSQL 16"),
139+ ai.NewModelTextMessage("noted"),
140+ })
141+ if _, err := newAskAgentCall(a).run(ctx, askAgentInput{Agent: "milo", Prompt: "specify the schema", WithContext: true}); err != nil {
142+ t.Fatal(err)
143+ }
144+ if !strings.Contains(a.got.prompt, "PostgreSQL 16") || !strings.HasSuffix(strings.TrimSpace(a.got.prompt), "specify the schema") {
145+ t.Errorf("prompt with context:\n%s", a.got.prompt)
146+ }
147+ if _, err := newAskAgentCall(a).run(ctx, askAgentInput{Agent: "milo", Prompt: "specify the schema"}); err != nil {
148+ t.Fatal(err)
149+ }
150+ if strings.Contains(a.got.prompt, "PostgreSQL") {
151+ t.Errorf("prompt without context carries it:\n%s", a.got.prompt)
152+ }
153+}
154+
155+// yoloOn sets yolo on for one test.
156+func yoloOn(t *testing.T) {
157+ t.Helper()
158+ old := config.Cfg.Yolo
159+ config.Cfg.Yolo = true
160+ t.Cleanup(func() { config.Cfg.Yolo = old })
161+}
162+
163+// askIdentity is an asked agent's identity as its config file would give it.
164+func askIdentity(title string) ask.Identity {
165+ return ask.Identity{Title: title, Model: "m", Provider: "llmman"}
166+}
modified internal/tui/ask.go +3 -5
@@ -21,9 +21,8 @@ import (
2121 func (r *runner) startAsk(p Prompt, req command.AskRequest) {
2222 r.t.AddUser(p.Display())
2323 r.t.Notice(ask.Label(req))
24- who := askSpeaker(req.Agent, ask.Identify(config.Cfg.MMConfigs[req.Agent]))
25- r.asking.Store(&who)
26- r.t.SetSpeaker(who.label)
24+ id := ask.Identify(config.Cfg.MMConfigs[req.Agent].Path)
25+ (&sink{r: r}).Speaking(req.Agent, id.Title, id.Model, id.Provider)
2726 ctx, cancel := context.WithCancel(context.Background())
2827 r.cancel = cancel
2928 r.cancelled.Store(false)
@@ -41,8 +40,7 @@ func (r *runner) startAsk(p Prompt, req command.AskRequest) {
4140 // when the other agent answered.
4241 func (r *runner) applyAsk(res outcome) {
4342 r.t.Close()
44- r.t.SetSpeaker("")
45- r.asking.Store(nil)
43+ (&sink{r: r}).Spoken()
4644 switch {
4745 case res.err != nil && res.cancelled:
4846 r.t.Notice("🛑 Ask aborted — nothing kept.")
@@ -21,9 +21,8 @@ import (
21 func (r *runner) startAsk(p Prompt, req command.AskRequest) {21 func (r *runner) startAsk(p Prompt, req command.AskRequest) {
22 r.t.AddUser(p.Display())22 r.t.AddUser(p.Display())
23 r.t.Notice(ask.Label(req))23 r.t.Notice(ask.Label(req))
24- who := askSpeaker(req.Agent, ask.Identify(config.Cfg.MMConfigs[req.Agent]))24+ id := ask.Identify(config.Cfg.MMConfigs[req.Agent].Path)
25- r.asking.Store(&who)25+ (&sink{r: r}).Speaking(req.Agent, id.Title, id.Model, id.Provider)
26- r.t.SetSpeaker(who.label)
27 ctx, cancel := context.WithCancel(context.Background())26 ctx, cancel := context.WithCancel(context.Background())
28 r.cancel = cancel27 r.cancel = cancel
29 r.cancelled.Store(false)28 r.cancelled.Store(false)
@@ -41,8 +40,7 @@ func (r *runner) startAsk(p Prompt, req command.AskRequest) {
41 // when the other agent answered.40 // when the other agent answered.
42 func (r *runner) applyAsk(res outcome) {41 func (r *runner) applyAsk(res outcome) {
43 r.t.Close()42 r.t.Close()
44- r.t.SetSpeaker("")43+ (&sink{r: r}).Spoken()
45- r.asking.Store(nil)
46 switch {44 switch {
47 case res.err != nil && res.cancelled:45 case res.err != nil && res.cancelled:
48 r.t.Notice("🛑 Ask aborted — nothing kept.")46 r.t.Notice("🛑 Ask aborted — nothing kept.")
modified internal/tui/ask_test.go +2 -2
@@ -46,7 +46,7 @@ func (a *fakeAsker) Run(ctx context.Context, req command.AskRequest, _ []*ai.Mes
4646 func askRunner(t *testing.T, a *fakeAsker) (*runner, *Transcript) {
4747 t.Helper()
4848 r, tr := newTestRunner(t, &fakeModel{})
49- config.Cfg.MMConfigs = map[string]string{"bob": "/nowhere/agent.bob.yaml"}
49+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "/nowhere/agent.bob.yaml"}}
5050 command.SetAgents([]string{"bob"})
5151 t.Cleanup(func() { command.SetAgents(nil) })
5252 r.opts.Asker = a
@@ -110,7 +110,7 @@ func TestWindowAskNamesTheAskedAgent(t *testing.T) {
110110 if err := os.WriteFile(path, []byte("provider: llmman\nmodel: hf.co/Menlo/Jan-nano-gguf:Q4_K_M\nacp:\n title: Jan (coding agent)\n"), 0o644); err != nil {
111111 t.Fatal(err)
112112 }
113- config.Cfg.MMConfigs["bob"] = path
113+ config.Cfg.MMConfigs["bob"] = config.AgentRef{Path: path}
114114 var during string
115115 a.during = func() { during = r.header() }
116116
@@ -46,7 +46,7 @@ func (a *fakeAsker) Run(ctx context.Context, req command.AskRequest, _ []*ai.Mes
46 func askRunner(t *testing.T, a *fakeAsker) (*runner, *Transcript) {46 func askRunner(t *testing.T, a *fakeAsker) (*runner, *Transcript) {
47 t.Helper()47 t.Helper()
48 r, tr := newTestRunner(t, &fakeModel{})48 r, tr := newTestRunner(t, &fakeModel{})
49- config.Cfg.MMConfigs = map[string]string{"bob": "/nowhere/agent.bob.yaml"}49+ config.Cfg.MMConfigs = map[string]config.AgentRef{"bob": {Path: "/nowhere/agent.bob.yaml"}}
50 command.SetAgents([]string{"bob"})50 command.SetAgents([]string{"bob"})
51 t.Cleanup(func() { command.SetAgents(nil) })51 t.Cleanup(func() { command.SetAgents(nil) })
52 r.opts.Asker = a52 r.opts.Asker = a
@@ -110,7 +110,7 @@ func TestWindowAskNamesTheAskedAgent(t *testing.T) {
110 if err := os.WriteFile(path, []byte("provider: llmman\nmodel: hf.co/Menlo/Jan-nano-gguf:Q4_K_M\nacp:\n title: Jan (coding agent)\n"), 0o644); err != nil {110 if err := os.WriteFile(path, []byte("provider: llmman\nmodel: hf.co/Menlo/Jan-nano-gguf:Q4_K_M\nacp:\n title: Jan (coding agent)\n"), 0o644); err != nil {
111 t.Fatal(err)111 t.Fatal(err)
112 }112 }
113- config.Cfg.MMConfigs["bob"] = path113+ config.Cfg.MMConfigs["bob"] = config.AgentRef{Path: path}
114 var during string114 var during string
115 a.during = func() { during = r.header() }115 a.during = func() { during = r.header() }
116 116
modified internal/tui/sink.go +21 -1
@@ -3,6 +3,7 @@ package tui
33 import (
44 "context"
55
6+ "mm/internal/ask"
67 "mm/internal/config"
78 "mm/internal/consent"
89 "mm/internal/ui"
@@ -15,7 +16,26 @@ import (
1516 // built-in tools, the MCP wrapper for its own.
1617 type sink struct{ r *runner }
1718
18-var _ ui.Sink = (*sink)(nil)
19+var (
20+ _ ui.Sink = (*sink)(nil)
21+ _ ui.Speaker = (*sink)(nil)
22+)
23+
24+// Speaking switches the header and the answers' label to the agent now
25+// answering — /ask, or the ask_agent tool — until Spoken.
26+func (k *sink) Speaking(name, title, model, provider string) {
27+ who := askSpeaker(name, ask.Identity{Title: title, Model: model, Provider: provider})
28+ k.r.asking.Store(&who)
29+ k.r.t.SetSpeaker(who.label)
30+ k.r.wake()
31+}
32+
33+// Spoken gives the header and the answers back to this agent.
34+func (k *sink) Spoken() {
35+ k.r.t.SetSpeaker("")
36+ k.r.asking.Store(nil)
37+ k.r.wake()
38+}
1939
2040 func (k *sink) Text(text string) {
2141 k.r.streamed.Store(true)
@@ -3,6 +3,7 @@ package tui
3 import (3 import (
4 "context"4 "context"
5 5
6+ "mm/internal/ask"
6 "mm/internal/config"7 "mm/internal/config"
7 "mm/internal/consent"8 "mm/internal/consent"
8 "mm/internal/ui"9 "mm/internal/ui"
@@ -15,7 +16,26 @@ import (
15 // built-in tools, the MCP wrapper for its own.16 // built-in tools, the MCP wrapper for its own.
16 type sink struct{ r *runner }17 type sink struct{ r *runner }
17 18
18-var _ ui.Sink = (*sink)(nil)19+var (
20+ _ ui.Sink = (*sink)(nil)
21+ _ ui.Speaker = (*sink)(nil)
22+)
23+
24+// Speaking switches the header and the answers' label to the agent now
25+// answering — /ask, or the ask_agent tool — until Spoken.
26+func (k *sink) Speaking(name, title, model, provider string) {
27+ who := askSpeaker(name, ask.Identity{Title: title, Model: model, Provider: provider})
28+ k.r.asking.Store(&who)
29+ k.r.t.SetSpeaker(who.label)
30+ k.r.wake()
31+}
32+
33+// Spoken gives the header and the answers back to this agent.
34+func (k *sink) Spoken() {
35+ k.r.t.SetSpeaker("")
36+ k.r.asking.Store(nil)
37+ k.r.wake()
38+}
19 39
20 func (k *sink) Text(text string) {40 func (k *sink) Text(text string) {
21 k.r.streamed.Store(true)41 k.r.streamed.Store(true)
modified internal/ui/ui.go +12 -0
@@ -101,6 +101,18 @@ type UsageSink interface {
101101 Usage(u Usage)
102102 }
103103
104+// Speaker is the optional side of a Sink that shows WHICH agent is answering:
105+// between Speaking and Spoken, what streams through the sink (text, tool
106+// calls) comes from another agent — the one the ask_agent tool asked — and
107+// the front end may label it so (the window does: header and answers).
108+type Speaker interface {
109+ // Speaking names the agent now answering: its mmConfigs name, and what
110+ // its config file says (acp.title, model, provider; empty when unknown).
111+ Speaking(name, title, model, provider string)
112+ // Spoken hands the floor back to this agent.
113+ Spoken()
114+}
115+
104116 // The active sink is set once per turn by the ACP façade and read by the tools
105117 // from Genkit's goroutines — hence the lock, not a plain variable.
106118 var (
@@ -101,6 +101,18 @@ type UsageSink interface {
101 Usage(u Usage)101 Usage(u Usage)
102 }102 }
103 103
104+// Speaker is the optional side of a Sink that shows WHICH agent is answering:
105+// between Speaking and Spoken, what streams through the sink (text, tool
106+// calls) comes from another agent — the one the ask_agent tool asked — and
107+// the front end may label it so (the window does: header and answers).
108+type Speaker interface {
109+ // Speaking names the agent now answering: its mmConfigs name, and what
110+ // its config file says (acp.title, model, provider; empty when unknown).
111+ Speaking(name, title, model, provider string)
112+ // Spoken hands the floor back to this agent.
113+ Spoken()
114+}
115+
104 // The active sink is set once per turn by the ACP façade and read by the tools116 // The active sink is set once per turn by the ACP façade and read by the tools
105 // from Genkit's goroutines — hence the lock, not a plain variable.117 // from Genkit's goroutines — hence the lock, not a plain variable.
106 var (118 var (
modified main.go +9 -3
@@ -226,7 +226,7 @@ func main() {
226226 // pressed Escape).
227227 loopDetector := detector.NewLoopDetector(10, 3).WithStop(3)
228228
229- set := assembleTools(e, loopDetector)
229+ set := assembleTools(e, loopDetector, asker)
230230 defer set.close()
231231 agentTools := set.tools
232232 set.warnings = append(set.warnings, ask.Missing()...)
@@ -311,8 +311,10 @@ type toolSet struct {
311311 // (built-in tools), so the two can be compared on the same prompts;
312312 // `fetch_content` behind `fetchContentTool`, off by default because it is the
313313 // one built-in tool that reaches the network; and `lsp` when `lsp.servers` names at least one server whose command is on
314-// the PATH — a missing server is a warning, never a refusal to start.
315-func assembleTools(e *engine.Engine, d *detector.LoopDetector) toolSet {
314+// the PATH — a missing server is a warning, never a refusal to start; and
315+// `ask_agent` when mmConfigs names at least one agent, over the same pool as
316+// /ask.
317+func assembleTools(e *engine.Engine, d *detector.LoopDetector, asker tools.AgentAsker) toolSet {
316318 set := toolSet{close: func() {}}
317319 if config.Cfg.BashTool {
318320 set.tools = append(set.tools, tools.Bash(e.G, d))
@@ -346,6 +348,10 @@ func assembleTools(e *engine.Engine, d *detector.LoopDetector) toolSet {
346348 set.tools = append(set.tools, tools.FetchContent(e.G, d, fetcher))
347349 set.names = append(set.names, "fetch_content")
348350 }
351+ if len(config.Cfg.MMConfigs) > 0 {
352+ set.tools = append(set.tools, tools.AskAgent(e.G, d, asker))
353+ set.names = append(set.names, "ask_agent")
354+ }
349355 assembleLSP(e, d, &set)
350356 assembleMCP(e, d, &set)
351357 return set
@@ -226,7 +226,7 @@ func main() {
226 // pressed Escape).226 // pressed Escape).
227 loopDetector := detector.NewLoopDetector(10, 3).WithStop(3)227 loopDetector := detector.NewLoopDetector(10, 3).WithStop(3)
228 228
229- set := assembleTools(e, loopDetector)229+ set := assembleTools(e, loopDetector, asker)
230 defer set.close()230 defer set.close()
231 agentTools := set.tools231 agentTools := set.tools
232 set.warnings = append(set.warnings, ask.Missing()...)232 set.warnings = append(set.warnings, ask.Missing()...)
@@ -311,8 +311,10 @@ type toolSet struct {
311 // (built-in tools), so the two can be compared on the same prompts;311 // (built-in tools), so the two can be compared on the same prompts;
312 // `fetch_content` behind `fetchContentTool`, off by default because it is the312 // `fetch_content` behind `fetchContentTool`, off by default because it is the
313 // one built-in tool that reaches the network; and `lsp` when `lsp.servers` names at least one server whose command is on313 // one built-in tool that reaches the network; and `lsp` when `lsp.servers` names at least one server whose command is on
314-// the PATH — a missing server is a warning, never a refusal to start.314+// the PATH — a missing server is a warning, never a refusal to start; and
315-func assembleTools(e *engine.Engine, d *detector.LoopDetector) toolSet {315+// `ask_agent` when mmConfigs names at least one agent, over the same pool as
316+// /ask.
317+func assembleTools(e *engine.Engine, d *detector.LoopDetector, asker tools.AgentAsker) toolSet {
316 set := toolSet{close: func() {}}318 set := toolSet{close: func() {}}
317 if config.Cfg.BashTool {319 if config.Cfg.BashTool {
318 set.tools = append(set.tools, tools.Bash(e.G, d))320 set.tools = append(set.tools, tools.Bash(e.G, d))
@@ -346,6 +348,10 @@ func assembleTools(e *engine.Engine, d *detector.LoopDetector) toolSet {
346 set.tools = append(set.tools, tools.FetchContent(e.G, d, fetcher))348 set.tools = append(set.tools, tools.FetchContent(e.G, d, fetcher))
347 set.names = append(set.names, "fetch_content")349 set.names = append(set.names, "fetch_content")
348 }350 }
351+ if len(config.Cfg.MMConfigs) > 0 {
352+ set.tools = append(set.tools, tools.AskAgent(e.G, d, asker))
353+ set.names = append(set.names, "ask_agent")
354+ }
349 assembleLSP(e, d, &set)355 assembleLSP(e, d, &set)
350 assembleMCP(e, d, &set)356 assembleMCP(e, d, &set)
351 return set357 return set
modified mini-mes/riker/README.en.md +23 -34
@@ -1,38 +1,26 @@
11 # Riker — one coordinator, two specialists
22
3-Three `mm` agents working together through `mm --ask`, called from the coordinator's skills.
3+Three `mm` agents. Riker talks with you and hands the work to the other two with the built-in `ask_agent` tool: it decides when to call whom, from the descriptions in `mmConfigs`. No skill is involved.
44
55 | Agent | Role | Model (llmman) | Reasoning |
66 |---|---|---|---|
7-| **riker** | Coordinator: talks with you, picks the skill, hands the work over | `hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M` | off (`enable_thinking: false`) |
8-| **milo** | Specifier: writes the spec, checks the code against it | `hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M` | on (`enable_thinking: true`) |
9-| **homer** | Coder: implements the spec, with tests | `hf.co/unsloth/Qwen3-Coder-Next-GGUF:Q4_K_M` | none (not a reasoning model) |
7+| **riker** | Coordinator: talks with you, picks the agent, writes its prompt | `hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M` | off (`enable_thinking: false`) |
8+| **milo** | Specifier: writes the spec into `specs/`, checks the code against it | `hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M` | on (`enable_thinking: true`) |
9+| **homer** | Coder: implements the spec, with tests | the one in `agent.homer.yaml` | depends on the model |
1010
1111 ## The flow
1212
13-Hand-offs go through **files**, in the project's `specs/` directory:
14-
1513 ```
16-you ──► riker ──write_file──► specs/<slug>.brief.md
17- └─ mm --ask milo ──► specs/<slug>.spec.md (+ milo.log)
18- └─ AGENT_YOLO=true mm --ask homer ──► code + specs/<slug>.report.md (+ homer.log)
19- └─ mm --ask milo ──► specs/<slug>.check.md
14+you ──► riker ──ask_agent──► milo : writes specs/<slug>.spec.md, answers with a summary
15+ ──ask_agent──► homer : implements the spec, answers with a report
16+ ──ask_agent──► milo : checks the code, criterion by criterion
2017 ```
2118
22-Nothing long travels on a command line (no quoting problems), and every step leaves a file you can read or fix before the next one.
23-
24-Each hand-off is a small script in `bin/` (`specify.sh`, `implement.sh`, `check.sh`, one argument: the slug) that the skills have riker run. On a first try, the 4B model miscopied the full `--ask` line: it dropped the quotes and `AGENT_YOLO=true`. It copies a short command correctly. The scripts also keep each agent's raw output (`.milo.out`, `.homer.out`) and extract only the document (the spec from its title on, the report from `# Report` on).
25-
26-## Riker's skills
19+The hand-off goes through a **file**: milo writes the spec, homer reads it. Every step leaves a file you can read or fix before the next one.
2720
28-| Skill | What it does |
29-|---|---|
30-| `specify` | Writes the brief, asks milo for the spec, summarises it, **stops** for your approval. |
31-| `implement` | Asks homer to implement a spec, relays his report. |
32-| `check-against-spec` | Asks milo to check the code, criterion by criterion. |
33-| `build-feature` | The three in a row; stops after the spec unless you say to go all the way. |
21+While milo or homer works, the window (`mm -tui`) shows it: the header becomes `mm [Homer (coder)] · <model> @ llmman · asked by mm [Riker (coordinator)]`, its tool calls stream under its name, and its answer is labelled `‣ mm [Homer (coder)]`.
3422
35-homer has his own skill, `skills-homer/implement-spec`: requirement by requirement, code and its tests, then a report in a fixed format.
23+Riker's procedure (pick a slug, ask for the spec, stop for your approval, have it implemented, have it checked, report) is in its system prompt. Say "go all the way" (or « sans t'arrêter ») to chain the three steps at once.
3624
3725 ## Run
3826
@@ -45,25 +33,26 @@ mm -config /path/to/mini-me/mini-mes/riker/agent.riker.yaml
4533
4634 Then, for instance: *"Create a Go CLI that converts temperatures"*, or *"specify a CSV export of the orders"*.
4735
48-You can also ask the agents directly: `/ask-milo …`, `/ask-homer …` in riker, or `mm -config agent.homer.yaml` from the project.
36+You can also ask the agents yourself: `/ask-milo …`, `/ask-homer …` in riker, or `mm -config agent.homer.yaml` from the project.
4937
5038 ## Permissions — read this
5139
52-- **riker has `yolo: false`**: every bash command and every write is submitted to you. Each hand-off to milo or homer is a bash command: you see it and approve it before it runs.
53-- **milo is read-only in practice**: an asked agent's tool calls follow the asker's `yolo`. `read_file` never asks; his writes are refused.
54-- **homer is started with `AGENT_YOLO=true`** by the `implement` skill: once you approve the command, he edits files and runs commands **without asking**. That is deliberate, since coding is his job, but work in a clean git repository so you can review and revert his changes.
55-- With `mm -p` nobody can answer: riker needs `AGENT_YOLO=true`, and then milo can write too.
40+- **riker has `yolo: false`**: every command, every write and **every hand-off** (`ask_agent`) is submitted to you. You see the prompt riker wrote for milo or homer before it goes.
41+- **milo and homer have `yolo: true` in riker's `mmConfigs`**: once you approve the hand-off, they write and run commands **without asking**. Milo's prompt keeps him to `specs/`; homer changes the project, so work in a clean git repository to review and revert his changes.
42+- To take an agent back under control, set `yolo: false` in its entry: each of its calls is then submitted to you.
43+- With `mm -p` nobody can answer: riker needs `AGENT_YOLO=true`.
5644
5745 ## Notable settings
5846
59-- **Riker's 30-minute timeouts** (`bashTimeout`, `watchdogTimeout`): one `--ask homer` command lasts the whole implementation. The price: a genuinely hung riker generation is only detected after 30 minutes.
60-- **Sampling**: each model starts from its own GGUF metadata (Gemma: 1.0 / 0.95 / top_k 64; Qwen3-Coder-Next: 1.0 / 0.95 / top_k 40). Riker's temperature is lowered to 0.3 (to copy exact commands), milo's to 0.7 (spec precision). Homer's is left as is.
47+- **`watchdogTimeout: 10m` for riker**: during `ask_agent`, each event of the asked agent (text, thought, tool call) counts as a sign of life. This limit only covers the longest silence: a large model loading and reading its first prompt.
48+- **Sampling**: each model starts from its own GGUF metadata. Riker's temperature is lowered to 0.3 (pick and name things exactly), milo's to 0.7 (spec precision).
6149 - **Gemma 4**: its template reads `enable_thinking`, off by default, with no effort levels.
62-- **Absolute `skillsDir`** for riker and homer: in the terminal, a relative path would be resolved from your project.
63-- **Memory**: up to three models loaded at once in llmman (~5 + ~16 + ~48 GB at Q4). llmman unloads a model after five idle minutes.
50+- **gopls for homer**: his `lsp` tool (definition, references, diagnostics, rename) uses gopls for `.go` files. Install it (`go install golang.org/x/tools/gopls@latest`, with `$(go env GOPATH)/bin` on the `PATH`); without it homer still starts, without the `lsp` tool.
51+- **`skillsDir`**: riker and milo have no skills (their procedure is in their prompt); homer has `skills-homer/` (`implement-spec`, `lsp-navigation`), an absolute path to fix if the repository moves.
52+- **Memory**: up to three models loaded at once in llmman. llmman unloads a model after five idle minutes.
6453
6554 ## See also
6655
67-- [Ask another agent from a skill](../../docs/en/how-to/ask-another-agent-from-a-skill.md)
68-- [CLI reference — ask mode](../../docs/en/reference/cli.md)
69-- [Turn reasoning off or tune it](../../docs/en/how-to/turn-reasoning-off.md)
56+- [Let your agent call other agents](../../docs/en/how-to/let-the-agent-call-other-agents.md)
57+- [The `ask_agent` tool](../../docs/en/reference/tools.md#ask_agent)
58+- [Asking another agent — explanation](../../docs/en/explanation/asking-another-agent.md)
@@ -1,38 +1,26 @@
1 # Riker — one coordinator, two specialists1 # Riker — one coordinator, two specialists
2 2
3-Three `mm` agents working together through `mm --ask`, called from the coordinator's skills.3+Three `mm` agents. Riker talks with you and hands the work to the other two with the built-in `ask_agent` tool: it decides when to call whom, from the descriptions in `mmConfigs`. No skill is involved.
4 4
5 | Agent | Role | Model (llmman) | Reasoning |5 | Agent | Role | Model (llmman) | Reasoning |
6 |---|---|---|---|6 |---|---|---|---|
7-| **riker** | Coordinator: talks with you, picks the skill, hands the work over | `hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M` | off (`enable_thinking: false`) |7+| **riker** | Coordinator: talks with you, picks the agent, writes its prompt | `hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M` | off (`enable_thinking: false`) |
8-| **milo** | Specifier: writes the spec, checks the code against it | `hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M` | on (`enable_thinking: true`) |8+| **milo** | Specifier: writes the spec into `specs/`, checks the code against it | `hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M` | on (`enable_thinking: true`) |
9-| **homer** | Coder: implements the spec, with tests | `hf.co/unsloth/Qwen3-Coder-Next-GGUF:Q4_K_M` | none (not a reasoning model) |9+| **homer** | Coder: implements the spec, with tests | the one in `agent.homer.yaml` | depends on the model |
10 10
11 ## The flow11 ## The flow
12 12
13-Hand-offs go through **files**, in the project's `specs/` directory:
14-
15 ```13 ```
16-you ──► riker ──write_file──► specs/<slug>.brief.md14+you ──► riker ──ask_agent──► milo : writes specs/<slug>.spec.md, answers with a summary
17- └─ mm --ask milo ──► specs/<slug>.spec.md (+ milo.log)15+ ──ask_agent──► homer : implements the spec, answers with a report
18- └─ AGENT_YOLO=true mm --ask homer ──► code + specs/<slug>.report.md (+ homer.log)16+ ──ask_agent──► milo : checks the code, criterion by criterion
19- └─ mm --ask milo ──► specs/<slug>.check.md
20 ```17 ```
21 18
22-Nothing long travels on a command line (no quoting problems), and every step leaves a file you can read or fix before the next one.19+The hand-off goes through a **file**: milo writes the spec, homer reads it. Every step leaves a file you can read or fix before the next one.
23-
24-Each hand-off is a small script in `bin/` (`specify.sh`, `implement.sh`, `check.sh`, one argument: the slug) that the skills have riker run. On a first try, the 4B model miscopied the full `--ask` line: it dropped the quotes and `AGENT_YOLO=true`. It copies a short command correctly. The scripts also keep each agent's raw output (`.milo.out`, `.homer.out`) and extract only the document (the spec from its title on, the report from `# Report` on).
25-
26-## Riker's skills
27 20
28-| Skill | What it does |21+While milo or homer works, the window (`mm -tui`) shows it: the header becomes `mm [Homer (coder)] · <model> @ llmman · asked by mm [Riker (coordinator)]`, its tool calls stream under its name, and its answer is labelled `‣ mm [Homer (coder)]`.
29-|---|---|
30-| `specify` | Writes the brief, asks milo for the spec, summarises it, **stops** for your approval. |
31-| `implement` | Asks homer to implement a spec, relays his report. |
32-| `check-against-spec` | Asks milo to check the code, criterion by criterion. |
33-| `build-feature` | The three in a row; stops after the spec unless you say to go all the way. |
34 22
35-homer has his own skill, `skills-homer/implement-spec`: requirement by requirement, code and its tests, then a report in a fixed format.23+Riker's procedure (pick a slug, ask for the spec, stop for your approval, have it implemented, have it checked, report) is in its system prompt. Say "go all the way" (or « sans t'arrêter ») to chain the three steps at once.
36 24
37 ## Run25 ## Run
38 26
@@ -45,25 +33,26 @@ mm -config /path/to/mini-me/mini-mes/riker/agent.riker.yaml
45 33
46 Then, for instance: *"Create a Go CLI that converts temperatures"*, or *"specify a CSV export of the orders"*.34 Then, for instance: *"Create a Go CLI that converts temperatures"*, or *"specify a CSV export of the orders"*.
47 35
48-You can also ask the agents directly: `/ask-milo …`, `/ask-homer …` in riker, or `mm -config agent.homer.yaml` from the project.36+You can also ask the agents yourself: `/ask-milo …`, `/ask-homer …` in riker, or `mm -config agent.homer.yaml` from the project.
49 37
50 ## Permissions — read this38 ## Permissions — read this
51 39
52-- **riker has `yolo: false`**: every bash command and every write is submitted to you. Each hand-off to milo or homer is a bash command: you see it and approve it before it runs.40+- **riker has `yolo: false`**: every command, every write and **every hand-off** (`ask_agent`) is submitted to you. You see the prompt riker wrote for milo or homer before it goes.
53-- **milo is read-only in practice**: an asked agent's tool calls follow the asker's `yolo`. `read_file` never asks; his writes are refused.41+- **milo and homer have `yolo: true` in riker's `mmConfigs`**: once you approve the hand-off, they write and run commands **without asking**. Milo's prompt keeps him to `specs/`; homer changes the project, so work in a clean git repository to review and revert his changes.
54-- **homer is started with `AGENT_YOLO=true`** by the `implement` skill: once you approve the command, he edits files and runs commands **without asking**. That is deliberate, since coding is his job, but work in a clean git repository so you can review and revert his changes.42+- To take an agent back under control, set `yolo: false` in its entry: each of its calls is then submitted to you.
55-- With `mm -p` nobody can answer: riker needs `AGENT_YOLO=true`, and then milo can write too.43+- With `mm -p` nobody can answer: riker needs `AGENT_YOLO=true`.
56 44
57 ## Notable settings45 ## Notable settings
58 46
59-- **Riker's 30-minute timeouts** (`bashTimeout`, `watchdogTimeout`): one `--ask homer` command lasts the whole implementation. The price: a genuinely hung riker generation is only detected after 30 minutes.47+- **`watchdogTimeout: 10m` for riker**: during `ask_agent`, each event of the asked agent (text, thought, tool call) counts as a sign of life. This limit only covers the longest silence: a large model loading and reading its first prompt.
60-- **Sampling**: each model starts from its own GGUF metadata (Gemma: 1.0 / 0.95 / top_k 64; Qwen3-Coder-Next: 1.0 / 0.95 / top_k 40). Riker's temperature is lowered to 0.3 (to copy exact commands), milo's to 0.7 (spec precision). Homer's is left as is.48+- **Sampling**: each model starts from its own GGUF metadata. Riker's temperature is lowered to 0.3 (pick and name things exactly), milo's to 0.7 (spec precision).
61 - **Gemma 4**: its template reads `enable_thinking`, off by default, with no effort levels.49 - **Gemma 4**: its template reads `enable_thinking`, off by default, with no effort levels.
62-- **Absolute `skillsDir`** for riker and homer: in the terminal, a relative path would be resolved from your project.50+- **gopls for homer**: his `lsp` tool (definition, references, diagnostics, rename) uses gopls for `.go` files. Install it (`go install golang.org/x/tools/gopls@latest`, with `$(go env GOPATH)/bin` on the `PATH`); without it homer still starts, without the `lsp` tool.
63-- **Memory**: up to three models loaded at once in llmman (~5 + ~16 + ~48 GB at Q4). llmman unloads a model after five idle minutes.51+- **`skillsDir`**: riker and milo have no skills (their procedure is in their prompt); homer has `skills-homer/` (`implement-spec`, `lsp-navigation`), an absolute path to fix if the repository moves.
52+- **Memory**: up to three models loaded at once in llmman. llmman unloads a model after five idle minutes.
64 53
65 ## See also54 ## See also
66 55
67-- [Ask another agent from a skill](../../docs/en/how-to/ask-another-agent-from-a-skill.md)56+- [Let your agent call other agents](../../docs/en/how-to/let-the-agent-call-other-agents.md)
68-- [CLI reference — ask mode](../../docs/en/reference/cli.md)57+- [The `ask_agent` tool](../../docs/en/reference/tools.md#ask_agent)
69-- [Turn reasoning off or tune it](../../docs/en/how-to/turn-reasoning-off.md)58+- [Asking another agent — explanation](../../docs/en/explanation/asking-another-agent.md)
modified mini-mes/riker/README.md +23 -34
@@ -1,38 +1,26 @@
11 # Riker — un coordinateur et deux spécialistes
22
3-Trois agents `mm` qui travaillent ensemble par `mm --ask`, appelé depuis les skills du coordinateur.
3+Trois agents `mm`. Riker parle avec vous et confie le travail aux deux autres avec l'outil intégré `ask_agent` : c'est lui qui décide quand appeler qui, d'après les descriptions de `mmConfigs`. Aucune skill n'est en jeu.
44
55 | Agent | Rôle | Modèle (llmman) | Raisonnement |
66 |---|---|---|---|
7-| **riker** | Coordinateur : parle avec vous, choisit la skill, passe la main | `hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M` | coupé (`enable_thinking: false`) |
8-| **milo** | Spécificateur : écrit la spec, vérifie le code contre elle | `hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M` | actif (`enable_thinking: true`) |
9-| **homer** | Codeur : implémente la spec, avec tests | `hf.co/unsloth/Qwen3-Coder-Next-GGUF:Q4_K_M` | aucun (modèle sans raisonnement) |
7+| **riker** | Coordinateur : parle avec vous, choisit l'agent, rédige son prompt | `hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M` | coupé (`enable_thinking: false`) |
8+| **milo** | Spécificateur : écrit la spec dans `specs/`, vérifie le code contre elle | `hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M` | actif (`enable_thinking: true`) |
9+| **homer** | Codeur : implémente la spec, avec tests | celui de `agent.homer.yaml` | selon le modèle |
1010
1111 ## Le circuit
1212
13-Les échanges passent par des **fichiers**, dans le dossier `specs/` du projet :
14-
1513 ```
16-vous ──► riker ──write_file──► specs/<slug>.brief.md
17- └─ mm --ask milo ──► specs/<slug>.spec.md (+ milo.log)
18- └─ AGENT_YOLO=true mm --ask homer ──► code + specs/<slug>.report.md (+ homer.log)
19- └─ mm --ask milo ──► specs/<slug>.check.md
14+vous ──► riker ──ask_agent──► milo : écrit specs/<slug>.spec.md, répond par un résumé
15+ ──ask_agent──► homer : implémente la spec, répond par un rapport
16+ ──ask_agent──► milo : vérifie le code, critère par critère
2017 ```
2118
22-Rien de long ne transite par une ligne de commande (pas de problème de guillemets), et chaque étape laisse un fichier que vous pouvez lire ou corriger avant la suivante.
23-
24-Chaque passage de main est un petit script de `bin/` (`specify.sh`, `implement.sh`, `check.sh`, un argument : le slug) que les skills font lancer à riker. Lors d'un premier essai, le modèle 4B recopiait mal la ligne `--ask` complète : il oubliait les guillemets et `AGENT_YOLO=true`. Une commande courte, il la recopie juste. Les scripts gardent aussi la sortie brute de chaque agent (`.milo.out`, `.homer.out`) et n'extraient que le document (la spec à partir de son titre, le rapport à partir de `# Report`).
25-
26-## Skills de riker
19+Le passage de main se fait par **fichier** : milo écrit la spec, homer la lit. Chaque étape laisse un fichier que vous pouvez lire ou corriger avant la suivante.
2720
28-| Skill | Ce qu'elle fait |
29-|---|---|
30-| `specify` | Écrit le brief, demande la spec à milo, la résume, **s'arrête** pour votre accord. |
31-| `implement` | Demande à homer d'implémenter une spec, rapporte son compte rendu. |
32-| `check-against-spec` | Demande à milo de vérifier le code critère par critère. |
33-| `build-feature` | Les trois à la suite ; s'arrête après la spec, sauf si vous dites d'aller jusqu'au bout. |
21+Pendant que milo ou homer travaille, la fenêtre (`mm -tui`) le montre : l'en-tête devient `mm [Homer (coder)] · <modèle> @ llmman · asked by mm [Riker (coordinator)]`, ses appels d'outils défilent sous son nom, et sa réponse est étiquetée `‣ mm [Homer (coder)]`.
3422
35-homer a sa propre skill, `skills-homer/implement-spec` : exigence par exigence, du code et ses tests, puis un rapport au format fixe.
23+La procédure de riker (choisir un slug, demander la spec, s'arrêter pour votre accord, faire implémenter, faire vérifier, rapporter) est dans son prompt système. Dites « sans t'arrêter » pour enchaîner les trois étapes d'un coup.
3624
3725 ## Lancer
3826
@@ -45,25 +33,26 @@ mm -config /chemin/vers/mini-me/mini-mes/riker/agent.riker.yaml
4533
4634 Puis, par exemple : *« Crée un CLI Go qui convertit des températures »*, ou *« spécifie un export CSV des commandes »*.
4735
48-Vous pouvez aussi interroger les agents directement : `/ask-milo …`, `/ask-homer …` dans riker, ou `mm -config agent.homer.yaml` depuis le projet.
36+Vous pouvez aussi interroger les agents vous-même : `/ask-milo …`, `/ask-homer …` dans riker, ou `mm -config agent.homer.yaml` depuis le projet.
4937
5038 ## Permissions — à lire
5139
52-- **riker a `yolo: false`** : chaque commande bash et chaque écriture vous est soumise. Chaque passage de main à milo ou homer est une commande bash : vous la voyez et l'approuvez avant qu'elle parte.
53-- **milo est en lecture seule de fait** : les appels d'outils d'un agent interrogé suivent le `yolo` de celui qui pose la question. `read_file` ne demande jamais ; ses écritures sont refusées.
54-- **homer est lancé avec `AGENT_YOLO=true`** par la skill `implement` : une fois la commande approuvée, il modifie des fichiers et lance des commandes **sans rien demander**. C'est voulu, puisque coder est son travail, mais travaillez dans un dépôt git propre pour pouvoir relire et annuler ses changements.
55-- Avec `mm -p`, personne ne peut répondre : il faut `AGENT_YOLO=true` pour riker, et alors milo peut écrire lui aussi.
40+- **riker a `yolo: false`** : chaque commande, chaque écriture et **chaque passage de main** (`ask_agent`) vous est soumis. Vous voyez le prompt que riker a écrit pour milo ou homer avant qu'il parte.
41+- **milo et homer ont `yolo: true` dans les `mmConfigs` de riker** : une fois le passage de main approuvé, ils écrivent et lancent des commandes **sans rien demander**. Le prompt de milo le limite à `specs/` ; homer modifie le projet, donc travaillez dans un dépôt git propre pour pouvoir relire et annuler ses changements.
42+- Pour reprendre la main sur un agent, mettez `yolo: false` dans son entrée : chacun de ses appels vous sera alors soumis.
43+- Avec `mm -p`, personne ne peut répondre : il faut `AGENT_YOLO=true` pour riker.
5644
5745 ## Réglages notables
5846
59-- **Délais de riker à 30 min** (`bashTimeout`, `watchdogTimeout`) : une commande `--ask homer` dure tout le temps de l'implémentation. En contrepartie, une génération de riker réellement bloquée n'est détectée qu'au bout de 30 minutes.
60-- **Échantillonnage** : chaque modèle part des valeurs de ses propres métadonnées GGUF (Gemma : 1.0 / 0.95 / top_k 64 ; Qwen3-Coder-Next : 1.0 / 0.95 / top_k 40). La température de riker est baissée à 0.3 (copier des commandes exactes), celle de milo à 0.7 (précision d'une spec). Celle de homer est laissée telle quelle.
47+- **`watchdogTimeout: 10m` pour riker** : pendant `ask_agent`, chaque événement de l'agent appelé (texte, pensée, appel d'outil) compte comme un signe de vie. Ce délai ne couvre donc que le plus long silence : un gros modèle qui se charge et lit son premier prompt.
48+- **Échantillonnage** : chaque modèle part des valeurs de ses propres métadonnées GGUF. La température de riker est baissée à 0.3 (choisir et nommer exactement), celle de milo à 0.7 (précision d'une spec).
6149 - **Gemma 4** : le template lit `enable_thinking`, désactivé par défaut, sans niveaux d'effort.
62-- **`skillsDir` absolu** pour riker et homer : en mode terminal, un chemin relatif serait résolu depuis votre projet.
63-- **Mémoire** : jusqu'à trois modèles chargés en même temps dans llmman (~5 + ~16 + ~48 Go en Q4). llmman décharge un modèle après cinq minutes d'inactivité.
50+- **gopls pour homer** : son outil `lsp` (définition, références, diagnostics, renommage) s'appuie sur gopls pour les fichiers `.go`. Installez-le (`go install golang.org/x/tools/gopls@latest`, avec `$(go env GOPATH)/bin` dans le `PATH`) ; sans lui, homer démarre quand même, sans l'outil `lsp`.
51+- **`skillsDir`** : riker et milo n'ont pas de skills (leur procédure est dans leur prompt) ; homer a `skills-homer/` (`implement-spec`, `lsp-navigation`), chemin absolu à corriger si le dépôt bouge.
52+- **Mémoire** : jusqu'à trois modèles chargés en même temps dans llmman. llmman décharge un modèle après cinq minutes d'inactivité.
6453
6554 ## Voir aussi
6655
67-- [Interroger un autre agent depuis une skill](../../docs/fr/how-to/ask-another-agent-from-a-skill.md)
68-- [Référence de la ligne de commande — mode ask](../../docs/fr/reference/cli.md)
69-- [Couper ou régler le raisonnement](../../docs/fr/how-to/turn-reasoning-off.md)
56+- [Laisser votre agent appeler d'autres agents](../../docs/fr/how-to/let-the-agent-call-other-agents.md)
57+- [L'outil `ask_agent`](../../docs/fr/reference/tools.md#ask_agent)
58+- [Interroger un autre agent — explication](../../docs/fr/explanation/asking-another-agent.md)
@@ -1,38 +1,26 @@
1 # Riker — un coordinateur et deux spécialistes1 # Riker — un coordinateur et deux spécialistes
2 2
3-Trois agents `mm` qui travaillent ensemble par `mm --ask`, appelé depuis les skills du coordinateur.3+Trois agents `mm`. Riker parle avec vous et confie le travail aux deux autres avec l'outil intégré `ask_agent` : c'est lui qui décide quand appeler qui, d'après les descriptions de `mmConfigs`. Aucune skill n'est en jeu.
4 4
5 | Agent | Rôle | Modèle (llmman) | Raisonnement |5 | Agent | Rôle | Modèle (llmman) | Raisonnement |
6 |---|---|---|---|6 |---|---|---|---|
7-| **riker** | Coordinateur : parle avec vous, choisit la skill, passe la main | `hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M` | coupé (`enable_thinking: false`) |7+| **riker** | Coordinateur : parle avec vous, choisit l'agent, rédige son prompt | `hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M` | coupé (`enable_thinking: false`) |
8-| **milo** | Spécificateur : écrit la spec, vérifie le code contre elle | `hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M` | actif (`enable_thinking: true`) |8+| **milo** | Spécificateur : écrit la spec dans `specs/`, vérifie le code contre elle | `hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M` | actif (`enable_thinking: true`) |
9-| **homer** | Codeur : implémente la spec, avec tests | `hf.co/unsloth/Qwen3-Coder-Next-GGUF:Q4_K_M` | aucun (modèle sans raisonnement) |9+| **homer** | Codeur : implémente la spec, avec tests | celui de `agent.homer.yaml` | selon le modèle |
10 10
11 ## Le circuit11 ## Le circuit
12 12
13-Les échanges passent par des **fichiers**, dans le dossier `specs/` du projet :
14-
15 ```13 ```
16-vous ──► riker ──write_file──► specs/<slug>.brief.md14+vous ──► riker ──ask_agent──► milo : écrit specs/<slug>.spec.md, répond par un résumé
17- └─ mm --ask milo ──► specs/<slug>.spec.md (+ milo.log)15+ ──ask_agent──► homer : implémente la spec, répond par un rapport
18- └─ AGENT_YOLO=true mm --ask homer ──► code + specs/<slug>.report.md (+ homer.log)16+ ──ask_agent──► milo : vérifie le code, critère par critère
19- └─ mm --ask milo ──► specs/<slug>.check.md
20 ```17 ```
21 18
22-Rien de long ne transite par une ligne de commande (pas de problème de guillemets), et chaque étape laisse un fichier que vous pouvez lire ou corriger avant la suivante.19+Le passage de main se fait par **fichier** : milo écrit la spec, homer la lit. Chaque étape laisse un fichier que vous pouvez lire ou corriger avant la suivante.
23-
24-Chaque passage de main est un petit script de `bin/` (`specify.sh`, `implement.sh`, `check.sh`, un argument : le slug) que les skills font lancer à riker. Lors d'un premier essai, le modèle 4B recopiait mal la ligne `--ask` complète : il oubliait les guillemets et `AGENT_YOLO=true`. Une commande courte, il la recopie juste. Les scripts gardent aussi la sortie brute de chaque agent (`.milo.out`, `.homer.out`) et n'extraient que le document (la spec à partir de son titre, le rapport à partir de `# Report`).
25-
26-## Skills de riker
27 20
28-| Skill | Ce qu'elle fait |21+Pendant que milo ou homer travaille, la fenêtre (`mm -tui`) le montre : l'en-tête devient `mm [Homer (coder)] · <modèle> @ llmman · asked by mm [Riker (coordinator)]`, ses appels d'outils défilent sous son nom, et sa réponse est étiquetée `‣ mm [Homer (coder)]`.
29-|---|---|
30-| `specify` | Écrit le brief, demande la spec à milo, la résume, **s'arrête** pour votre accord. |
31-| `implement` | Demande à homer d'implémenter une spec, rapporte son compte rendu. |
32-| `check-against-spec` | Demande à milo de vérifier le code critère par critère. |
33-| `build-feature` | Les trois à la suite ; s'arrête après la spec, sauf si vous dites d'aller jusqu'au bout. |
34 22
35-homer a sa propre skill, `skills-homer/implement-spec` : exigence par exigence, du code et ses tests, puis un rapport au format fixe.23+La procédure de riker (choisir un slug, demander la spec, s'arrêter pour votre accord, faire implémenter, faire vérifier, rapporter) est dans son prompt système. Dites « sans t'arrêter » pour enchaîner les trois étapes d'un coup.
36 24
37 ## Lancer25 ## Lancer
38 26
@@ -45,25 +33,26 @@ mm -config /chemin/vers/mini-me/mini-mes/riker/agent.riker.yaml
45 33
46 Puis, par exemple : *« Crée un CLI Go qui convertit des températures »*, ou *« spécifie un export CSV des commandes »*.34 Puis, par exemple : *« Crée un CLI Go qui convertit des températures »*, ou *« spécifie un export CSV des commandes »*.
47 35
48-Vous pouvez aussi interroger les agents directement : `/ask-milo …`, `/ask-homer …` dans riker, ou `mm -config agent.homer.yaml` depuis le projet.36+Vous pouvez aussi interroger les agents vous-même : `/ask-milo …`, `/ask-homer …` dans riker, ou `mm -config agent.homer.yaml` depuis le projet.
49 37
50 ## Permissions — à lire38 ## Permissions — à lire
51 39
52-- **riker a `yolo: false`** : chaque commande bash et chaque écriture vous est soumise. Chaque passage de main à milo ou homer est une commande bash : vous la voyez et l'approuvez avant qu'elle parte.40+- **riker a `yolo: false`** : chaque commande, chaque écriture et **chaque passage de main** (`ask_agent`) vous est soumis. Vous voyez le prompt que riker a écrit pour milo ou homer avant qu'il parte.
53-- **milo est en lecture seule de fait** : les appels d'outils d'un agent interrogé suivent le `yolo` de celui qui pose la question. `read_file` ne demande jamais ; ses écritures sont refusées.41+- **milo et homer ont `yolo: true` dans les `mmConfigs` de riker** : une fois le passage de main approuvé, ils écrivent et lancent des commandes **sans rien demander**. Le prompt de milo le limite à `specs/` ; homer modifie le projet, donc travaillez dans un dépôt git propre pour pouvoir relire et annuler ses changements.
54-- **homer est lancé avec `AGENT_YOLO=true`** par la skill `implement` : une fois la commande approuvée, il modifie des fichiers et lance des commandes **sans rien demander**. C'est voulu, puisque coder est son travail, mais travaillez dans un dépôt git propre pour pouvoir relire et annuler ses changements.42+- Pour reprendre la main sur un agent, mettez `yolo: false` dans son entrée : chacun de ses appels vous sera alors soumis.
55-- Avec `mm -p`, personne ne peut répondre : il faut `AGENT_YOLO=true` pour riker, et alors milo peut écrire lui aussi.43+- Avec `mm -p`, personne ne peut répondre : il faut `AGENT_YOLO=true` pour riker.
56 44
57 ## Réglages notables45 ## Réglages notables
58 46
59-- **Délais de riker à 30 min** (`bashTimeout`, `watchdogTimeout`) : une commande `--ask homer` dure tout le temps de l'implémentation. En contrepartie, une génération de riker réellement bloquée n'est détectée qu'au bout de 30 minutes.47+- **`watchdogTimeout: 10m` pour riker** : pendant `ask_agent`, chaque événement de l'agent appelé (texte, pensée, appel d'outil) compte comme un signe de vie. Ce délai ne couvre donc que le plus long silence : un gros modèle qui se charge et lit son premier prompt.
60-- **Échantillonnage** : chaque modèle part des valeurs de ses propres métadonnées GGUF (Gemma : 1.0 / 0.95 / top_k 64 ; Qwen3-Coder-Next : 1.0 / 0.95 / top_k 40). La température de riker est baissée à 0.3 (copier des commandes exactes), celle de milo à 0.7 (précision d'une spec). Celle de homer est laissée telle quelle.48+- **Échantillonnage** : chaque modèle part des valeurs de ses propres métadonnées GGUF. La température de riker est baissée à 0.3 (choisir et nommer exactement), celle de milo à 0.7 (précision d'une spec).
61 - **Gemma 4** : le template lit `enable_thinking`, désactivé par défaut, sans niveaux d'effort.49 - **Gemma 4** : le template lit `enable_thinking`, désactivé par défaut, sans niveaux d'effort.
62-- **`skillsDir` absolu** pour riker et homer : en mode terminal, un chemin relatif serait résolu depuis votre projet.50+- **gopls pour homer** : son outil `lsp` (définition, références, diagnostics, renommage) s'appuie sur gopls pour les fichiers `.go`. Installez-le (`go install golang.org/x/tools/gopls@latest`, avec `$(go env GOPATH)/bin` dans le `PATH`) ; sans lui, homer démarre quand même, sans l'outil `lsp`.
63-- **Mémoire** : jusqu'à trois modèles chargés en même temps dans llmman (~5 + ~16 + ~48 Go en Q4). llmman décharge un modèle après cinq minutes d'inactivité.51+- **`skillsDir`** : riker et milo n'ont pas de skills (leur procédure est dans leur prompt) ; homer a `skills-homer/` (`implement-spec`, `lsp-navigation`), chemin absolu à corriger si le dépôt bouge.
52+- **Mémoire** : jusqu'à trois modèles chargés en même temps dans llmman. llmman décharge un modèle après cinq minutes d'inactivité.
64 53
65 ## Voir aussi54 ## Voir aussi
66 55
67-- [Interroger un autre agent depuis une skill](../../docs/fr/how-to/ask-another-agent-from-a-skill.md)56+- [Laisser votre agent appeler d'autres agents](../../docs/fr/how-to/let-the-agent-call-other-agents.md)
68-- [Référence de la ligne de commande — mode ask](../../docs/fr/reference/cli.md)57+- [L'outil `ask_agent`](../../docs/fr/reference/tools.md#ask_agent)
69-- [Couper ou régler le raisonnement](../../docs/fr/how-to/turn-reasoning-off.md)58+- [Interroger un autre agent — explication](../../docs/fr/explanation/asking-another-agent.md)
modified mini-mes/riker/agent.homer.yaml +31 -18
@@ -1,5 +1,5 @@
1-# Homer — the coder. Asked by riker (`AGENT_YOLO=true mm --ask homer`) to
2-# implement a specification written by milo. You can also run him directly:
1+# Homer — the coder. Asked by riker through `ask_agent` (or by you with
2+# /ask-homer) to implement a specification written by milo. You can also run him directly:
33 # `mm -config agent.homer.yaml`, from the project.
44 #
55 # Started by riker as a child process (`mm -acp -config agent.homer.yaml`)
@@ -14,6 +14,7 @@ provider: llmman
1414 # long edit/test loops. ~48 GB at Q4_K_M.
1515 #model: hf.co/unsloth/Qwen3-Coder-Next-GGUF:Q4_K_M
1616 model: hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M
17+#model: hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M
1718
1819 baseUrl: http://127.0.0.1:17434/v1
1920 fallback: http://host.docker.internal:17434/v1
@@ -71,9 +72,9 @@ displayCommands: true
7172
7273 # ─── Tools ───────────────────────────────────────────────────────────────────
7374
74-# When riker asks, riker's yolo decides homer's calls — and riker's skill
75-# starts the ask with AGENT_YOLO=true, so homer works without interruption
76-# once you approved the hand-off. When you run homer directly, this applies:
75+# When riker asks, homer's calls are decided by his entry in riker's
76+# mmConfigs (`yolo: true`): he works without interruption once you approved
77+# the hand-off. When you run homer directly, this applies:
7778 # every change waits for you.
7879 yolo: false
7980
@@ -88,18 +89,26 @@ planTool: false
8889 # Reading a library's documentation on the web.
8990 fetchContentTool: true
9091
91-# Uncomment the servers you have, for definition / references / diagnostics.
92-# lsp:
93-# servers:
94-# - name: gopls
95-# command: gopls
96-# extensions: [".go"]
97-# - name: typescript
98-# command: typescript-language-server
99-# args: ["--stdio"]
100-# extensions: [".ts", ".tsx", ".js", ".jsx"]
101-
102-# One skill: implement-spec. Absolute: a relative path would follow this
92+# The `lsp` tool, backed by gopls for .go files: definition, references,
93+# hover, symbols, diagnostics, rename across the module — what the compiler
94+# knows, without a build. Install it with
95+# go install golang.org/x/tools/gopls@latest
96+# and make sure $(go env GOPATH)/bin is on the PATH. Missing = a start-up
97+# warning (on stderr when riker asks) and no lsp tool; everything else works.
98+# gopls starts on homer's first lsp call, in the project directory.
99+lsp:
100+ servers:
101+ - name: gopls
102+ command: gopls
103+ extensions: [".go"]
104+ # Uncomment for TypeScript / JavaScript projects:
105+ # - name: typescript
106+ # command: typescript-language-server
107+ # args: ["--stdio"]
108+ # extensions: [".ts", ".tsx", ".js", ".jsx"]
109+
110+# Two skills: implement-spec, and lsp-navigation (how to use the lsp tool).
111+# Absolute: a relative path would follow this
103112 # file when riker asks (ACP mode) but the current directory when you run
104113 # homer directly in a terminal. Adjust it if you move the repository.
105114 skillsDir: /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/skills-homer
@@ -119,7 +128,7 @@ context:
119128
120129 acp:
121130 name: homer
122- title: Homer (coder)
131+ title: 🐻‍❄️ Homer (coder)
123132
124133 system: |
125134 Your name is Homer. You are a senior software engineer working in a
@@ -134,6 +143,10 @@ system: |
134143 - `read_file`, `edit_file`, `write_file` read and change files. Read before
135144 you write. Change existing files with `edit_file` (`old` copied exactly,
136145 unique); `write_file` only for new files or a deliberate full rewrite.
146+ - `lsp` asks gopls about Go code: where a symbol is defined, who uses it,
147+ its type, what the compiler reports in a file, and a safe rename. Prefer
148+ it to grep for Go symbols; read the `lsp-navigation` skill before your
149+ first lsp call.
137150 - `fetch_content` reads documentation pages.
138151
139152 RULES
@@ -1,5 +1,5 @@
1-# Homer — the coder. Asked by riker (`AGENT_YOLO=true mm --ask homer`) to1+# Homer — the coder. Asked by riker through `ask_agent` (or by you with
2-# implement a specification written by milo. You can also run him directly:2+# /ask-homer) to implement a specification written by milo. You can also run him directly:
3 # `mm -config agent.homer.yaml`, from the project.3 # `mm -config agent.homer.yaml`, from the project.
4 #4 #
5 # Started by riker as a child process (`mm -acp -config agent.homer.yaml`)5 # Started by riker as a child process (`mm -acp -config agent.homer.yaml`)
@@ -14,6 +14,7 @@ provider: llmman
14 # long edit/test loops. ~48 GB at Q4_K_M.14 # long edit/test loops. ~48 GB at Q4_K_M.
15 #model: hf.co/unsloth/Qwen3-Coder-Next-GGUF:Q4_K_M15 #model: hf.co/unsloth/Qwen3-Coder-Next-GGUF:Q4_K_M
16 model: hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M16 model: hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M
17+#model: hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M
17 18
18 baseUrl: http://127.0.0.1:17434/v119 baseUrl: http://127.0.0.1:17434/v1
19 fallback: http://host.docker.internal:17434/v120 fallback: http://host.docker.internal:17434/v1
@@ -71,9 +72,9 @@ displayCommands: true
71 72
72 # ─── Tools ───────────────────────────────────────────────────────────────────73 # ─── Tools ───────────────────────────────────────────────────────────────────
73 74
74-# When riker asks, riker's yolo decides homer's calls — and riker's skill75+# When riker asks, homer's calls are decided by his entry in riker's
75-# starts the ask with AGENT_YOLO=true, so homer works without interruption76+# mmConfigs (`yolo: true`): he works without interruption once you approved
76-# once you approved the hand-off. When you run homer directly, this applies:77+# the hand-off. When you run homer directly, this applies:
77 # every change waits for you.78 # every change waits for you.
78 yolo: false79 yolo: false
79 80
@@ -88,18 +89,26 @@ planTool: false
88 # Reading a library's documentation on the web.89 # Reading a library's documentation on the web.
89 fetchContentTool: true90 fetchContentTool: true
90 91
91-# Uncomment the servers you have, for definition / references / diagnostics.92+# The `lsp` tool, backed by gopls for .go files: definition, references,
92-# lsp:93+# hover, symbols, diagnostics, rename across the module — what the compiler
93-# servers:94+# knows, without a build. Install it with
94-# - name: gopls95+# go install golang.org/x/tools/gopls@latest
95-# command: gopls96+# and make sure $(go env GOPATH)/bin is on the PATH. Missing = a start-up
96-# extensions: [".go"]97+# warning (on stderr when riker asks) and no lsp tool; everything else works.
97-# - name: typescript98+# gopls starts on homer's first lsp call, in the project directory.
98-# command: typescript-language-server99+lsp:
99-# args: ["--stdio"]100+ servers:
100-# extensions: [".ts", ".tsx", ".js", ".jsx"]101+ - name: gopls
101-102+ command: gopls
102-# One skill: implement-spec. Absolute: a relative path would follow this103+ extensions: [".go"]
104+ # Uncomment for TypeScript / JavaScript projects:
105+ # - name: typescript
106+ # command: typescript-language-server
107+ # args: ["--stdio"]
108+ # extensions: [".ts", ".tsx", ".js", ".jsx"]
109+
110+# Two skills: implement-spec, and lsp-navigation (how to use the lsp tool).
111+# Absolute: a relative path would follow this
103 # file when riker asks (ACP mode) but the current directory when you run112 # file when riker asks (ACP mode) but the current directory when you run
104 # homer directly in a terminal. Adjust it if you move the repository.113 # homer directly in a terminal. Adjust it if you move the repository.
105 skillsDir: /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/skills-homer114 skillsDir: /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/skills-homer
@@ -119,7 +128,7 @@ context:
119 128
120 acp:129 acp:
121 name: homer130 name: homer
122- title: Homer (coder)131+ title: 🐻‍❄️ Homer (coder)
123 132
124 system: |133 system: |
125 Your name is Homer. You are a senior software engineer working in a134 Your name is Homer. You are a senior software engineer working in a
@@ -134,6 +143,10 @@ system: |
134 - `read_file`, `edit_file`, `write_file` read and change files. Read before143 - `read_file`, `edit_file`, `write_file` read and change files. Read before
135 you write. Change existing files with `edit_file` (`old` copied exactly,144 you write. Change existing files with `edit_file` (`old` copied exactly,
136 unique); `write_file` only for new files or a deliberate full rewrite.145 unique); `write_file` only for new files or a deliberate full rewrite.
146+ - `lsp` asks gopls about Go code: where a symbol is defined, who uses it,
147+ its type, what the compiler reports in a file, and a safe rename. Prefer
148+ it to grep for Go symbols; read the `lsp-navigation` skill before your
149+ first lsp call.
137 - `fetch_content` reads documentation pages.150 - `fetch_content` reads documentation pages.
138 151
139 RULES152 RULES
modified mini-mes/riker/agent.milo.yaml +29 -25
@@ -1,9 +1,9 @@
1-# Milo — the specifier. Asked by riker (`mm --ask milo`), never used directly
2-# in normal work (you can: `mm -config agent.milo.yaml`).
1+# Milo — the specifier. Asked by riker through `ask_agent` (or by you with
2+# /ask-milo); you can also run him directly: `mm -config agent.milo.yaml`.
33 #
4-# Two jobs: turn a brief into a specification, and check an implementation
5-# against a specification. His answer is redirected to a file by riker's
6-# skill, so he answers with the document and nothing else.
4+# Two jobs: write a specification into specs/<slug>.spec.md, and check an
5+# implementation against a specification. His answer goes back to riker as
6+# the tool's result: a short summary, or the verdict.
77 #
88 # Started by riker as a child process (`mm -acp -config agent.milo.yaml`) in
99 # riker's working directory — the project.
@@ -68,17 +68,16 @@ displayCommands: true
6868
6969 # ─── Tools ───────────────────────────────────────────────────────────────────
7070
71-# Riker's yolo decides milo's calls. read_file never asks, so it always
72-# works; write_file / edit_file are refused (riker runs with yolo false and
73-# does not override it for milo): milo is read-only in practice, and his
74-# prompt says so too.
71+# When riker asks, milo's calls are decided by his entry in riker's
72+# mmConfigs (`yolo: true`: his write_file of the spec runs without asking).
73+# This value applies only when you run milo directly.
7574 yolo: false
7675
77-# No bash: a specifier reads, he does not run things.
76+# No bash: a specifier reads and writes documents, he does not run things.
7877 bashTool: false
7978
80-# read_file — to read the brief and the code it mentions. (write_file and
81-# edit_file come with it; see yolo above.)
79+# read_file to read the code a request is about, write_file to save the spec.
80+# His prompt keeps his writes to specs/.
8281 editTools: true
8382
8483 planTool: false
@@ -103,22 +102,23 @@ context:
103102
104103 acp:
105104 name: milo
106- title: Milo (specifier)
105+ title: 🐼 Milo (specifier)
107106
108107 system: |
109108 Your name is Milo. You are a software specifier. You are asked by a
110- coordinator, not by a person: your answer is saved to a file as it is.
109+ coordinator, not by a person; your answer goes back to it.
111110
112111 YOUR TOOLS
113- `read_file` only. Read the files you are told to read, and the files they
114- mention. Never call `write_file` or `edit_file`: they are refused. Your
115- answer IS the document.
112+ `read_file` to read the project's files. `write_file` ONLY to save a
113+ specification under specs/ — never anywhere else, never code. Never call
114+ `edit_file`.
116115
117116 JOB 1 — WRITE A SPECIFICATION
118- When asked to specify from a brief: read the brief, read the existing code
119- it names, then answer with the specification in Markdown. Start with a
120- line `# <Title>`, then these eight `##` headings, written exactly as
121- shown here — the heading text only, nothing after it:
117+ When asked to specify a request: read the existing code it names, then
118+ write the specification with `write_file`, in Markdown, to the path you
119+ were given (specs/<slug>.spec.md). Start the document with a line
120+ `# <Title>`, then these eight `##` headings, written exactly as shown
121+ here — the heading text only, nothing after it:
122122
123123 ## Goal
124124 ## Context
@@ -138,14 +138,18 @@ system: |
138138 - Edge cases and errors: inputs and failures, and the expected behaviour.
139139 - Acceptance criteria: numbered A1, A2…, Given / When / Then, each
140140 traceable to a requirement, with exact commands and outputs when the
141- brief gives examples.
141+ request gives examples.
142142 - Out of scope: what NOT to do.
143- - Open questions: only what the brief truly leaves undecided, each with
143+ - Open questions: only what the request truly leaves undecided, each with
144144 the default you chose so the coder is never blocked.
145145
146146 Write for a coder who will read nothing else: concrete, no filler, no
147147 code beyond signatures and short examples. Language and stack: those of
148- the project, or of the brief.
148+ the project, or of the request.
149+
150+ Then answer with a SHORT summary, not the document: the file you wrote,
151+ the goal in one sentence, the number of requirements and of acceptance
152+ criteria, and the open questions with the defaults you chose.
149153
150154 JOB 2 — CHECK AN IMPLEMENTATION
151155 When asked to check code against a specification: read the spec, then the
@@ -155,5 +159,5 @@ system: |
155159 from what you read.
156160
157161 RULES
158- - No greeting, no preamble, no closing remark: the document only.
162+ - No greeting, no preamble, no closing remark.
159163 - Answer in English.
@@ -1,9 +1,9 @@
1-# Milo — the specifier. Asked by riker (`mm --ask milo`), never used directly1+# Milo — the specifier. Asked by riker through `ask_agent` (or by you with
2-# in normal work (you can: `mm -config agent.milo.yaml`).2+# /ask-milo); you can also run him directly: `mm -config agent.milo.yaml`.
3 #3 #
4-# Two jobs: turn a brief into a specification, and check an implementation4+# Two jobs: write a specification into specs/<slug>.spec.md, and check an
5-# against a specification. His answer is redirected to a file by riker's5+# implementation against a specification. His answer goes back to riker as
6-# skill, so he answers with the document and nothing else.6+# the tool's result: a short summary, or the verdict.
7 #7 #
8 # Started by riker as a child process (`mm -acp -config agent.milo.yaml`) in8 # Started by riker as a child process (`mm -acp -config agent.milo.yaml`) in
9 # riker's working directory — the project.9 # riker's working directory — the project.
@@ -68,17 +68,16 @@ displayCommands: true
68 68
69 # ─── Tools ───────────────────────────────────────────────────────────────────69 # ─── Tools ───────────────────────────────────────────────────────────────────
70 70
71-# Riker's yolo decides milo's calls. read_file never asks, so it always71+# When riker asks, milo's calls are decided by his entry in riker's
72-# works; write_file / edit_file are refused (riker runs with yolo false and72+# mmConfigs (`yolo: true`: his write_file of the spec runs without asking).
73-# does not override it for milo): milo is read-only in practice, and his73+# This value applies only when you run milo directly.
74-# prompt says so too.
75 yolo: false74 yolo: false
76 75
77-# No bash: a specifier reads, he does not run things.76+# No bash: a specifier reads and writes documents, he does not run things.
78 bashTool: false77 bashTool: false
79 78
80-# read_file — to read the brief and the code it mentions. (write_file and79+# read_file to read the code a request is about, write_file to save the spec.
81-# edit_file come with it; see yolo above.)80+# His prompt keeps his writes to specs/.
82 editTools: true81 editTools: true
83 82
84 planTool: false83 planTool: false
@@ -103,22 +102,23 @@ context:
103 102
104 acp:103 acp:
105 name: milo104 name: milo
106- title: Milo (specifier)105+ title: 🐼 Milo (specifier)
107 106
108 system: |107 system: |
109 Your name is Milo. You are a software specifier. You are asked by a108 Your name is Milo. You are a software specifier. You are asked by a
110- coordinator, not by a person: your answer is saved to a file as it is.109+ coordinator, not by a person; your answer goes back to it.
111 110
112 YOUR TOOLS111 YOUR TOOLS
113- `read_file` only. Read the files you are told to read, and the files they112+ `read_file` to read the project's files. `write_file` ONLY to save a
114- mention. Never call `write_file` or `edit_file`: they are refused. Your113+ specification under specs/ — never anywhere else, never code. Never call
115- answer IS the document.114+ `edit_file`.
116 115
117 JOB 1 — WRITE A SPECIFICATION116 JOB 1 — WRITE A SPECIFICATION
118- When asked to specify from a brief: read the brief, read the existing code117+ When asked to specify a request: read the existing code it names, then
119- it names, then answer with the specification in Markdown. Start with a118+ write the specification with `write_file`, in Markdown, to the path you
120- line `# <Title>`, then these eight `##` headings, written exactly as119+ were given (specs/<slug>.spec.md). Start the document with a line
121- shown here — the heading text only, nothing after it:120+ `# <Title>`, then these eight `##` headings, written exactly as shown
121+ here — the heading text only, nothing after it:
122 122
123 ## Goal123 ## Goal
124 ## Context124 ## Context
@@ -138,14 +138,18 @@ system: |
138 - Edge cases and errors: inputs and failures, and the expected behaviour.138 - Edge cases and errors: inputs and failures, and the expected behaviour.
139 - Acceptance criteria: numbered A1, A2…, Given / When / Then, each139 - Acceptance criteria: numbered A1, A2…, Given / When / Then, each
140 traceable to a requirement, with exact commands and outputs when the140 traceable to a requirement, with exact commands and outputs when the
141- brief gives examples.141+ request gives examples.
142 - Out of scope: what NOT to do.142 - Out of scope: what NOT to do.
143- - Open questions: only what the brief truly leaves undecided, each with143+ - Open questions: only what the request truly leaves undecided, each with
144 the default you chose so the coder is never blocked.144 the default you chose so the coder is never blocked.
145 145
146 Write for a coder who will read nothing else: concrete, no filler, no146 Write for a coder who will read nothing else: concrete, no filler, no
147 code beyond signatures and short examples. Language and stack: those of147 code beyond signatures and short examples. Language and stack: those of
148- the project, or of the brief.148+ the project, or of the request.
149+
150+ Then answer with a SHORT summary, not the document: the file you wrote,
151+ the goal in one sentence, the number of requirements and of acceptance
152+ criteria, and the open questions with the defaults you chose.
149 153
150 JOB 2 — CHECK AN IMPLEMENTATION154 JOB 2 — CHECK AN IMPLEMENTATION
151 When asked to check code against a specification: read the spec, then the155 When asked to check code against a specification: read the spec, then the
@@ -155,5 +159,5 @@ system: |
155 from what you read.159 from what you read.
156 160
157 RULES161 RULES
158- - No greeting, no preamble, no closing remark: the document only.162+ - No greeting, no preamble, no closing remark.
159 - Answer in English.163 - Answer in English.
modified mini-mes/riker/agent.riker.yaml +99 -75
@@ -1,19 +1,21 @@
11 # Riker — the coordinator. A small, fast model that talks with you and hands
2-# the work to two specialists through `mm --ask` (called from its skills):
2+# the work to two specialists with the built-in `ask_agent` tool:
33 #
4-# milo — the specifier (Gemma 4 26B-A4B, reasoning on): turns a request
5-# into a written specification, then checks code against it.
4+# milo — the specifier (Gemma 4 26B-A4B, reasoning on): writes a
5+# specification into specs/, then checks code against it.
66 # homer — the coder (Qwen3-Coder-Next 80B-A3B): implements a specification.
77 #
8-# The hand-off is FILE-based, in the project's specs/ directory:
9-# specs/<slug>.brief.md written by riker (what the user asked)
10-# specs/<slug>.spec.md written by milo (the specification)
11-# code written by homer (from the spec)
12-# Nobody has to quote a long text on a command line, and every step leaves a
13-# file you can read, edit, or hand to someone else.
8+# Riker decides when to call whom, from the descriptions below; no skill is
9+# involved. While milo or homer works, the window says so: its header names
10+# the agent and its model ("… asked by mm [Riker (coordinator)]"), and its
11+# answers are labelled with its name.
1412 #
15-# Run it FROM THE PROJECT to work on — milo and homer work in the same
16-# directory as riker's bash:
13+# The hand-off is FILE-based, in the project's specs/ directory: milo writes
14+# specs/<slug>.spec.md, homer implements it, milo checks the code against it.
15+# Every step leaves a file you can read or edit before the next one.
16+#
17+# Run it FROM THE PROJECT to work on — milo and homer work in the directory
18+# riker was started in:
1719 #
1820 # cd ~/src/my-project
1921 # mm -config /path/to/mini-mes/riker/agent.riker.yaml
@@ -28,7 +30,8 @@
2830 provider: llmman
2931
3032 # Gemma 4 E4B (7.5B parameters, ~4B active per token): quick answers, good
31-# enough to pick a skill and run one command. It does not design or code.
33+# enough to pick an agent and write it a clear prompt. It does not design or
34+# code.
3235 model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
3336
3437 # `llmman serve`'s default bind, then the host's llmman seen from a Docker
@@ -36,12 +39,35 @@ model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
3639 baseUrl: http://127.0.0.1:17434/v1
3740 fallback: http://host.docker.internal:17434/v1
3841
39-# The agents riker can ask. Names become /ask-milo, /ask-homer (and
40-# /ask-with-context-…) for YOU to type, and `"$MM_BIN" --ask <name>` for the
41-# scripts in bin/. Paths are relative to this file.
42+# The agents riker can hand work to. Each one is:
43+# - a tool target: `ask_agent` (declared because this list is not empty)
44+# shows riker each agent's `description` — that is how it decides;
45+# - a command for YOU: /ask-milo, /ask-homer, /ask-with-context-…
46+# Paths are relative to this file.
47+#
48+# `yolo` decides that agent's own tool calls instead of riker's yolo:
49+# true = they run without asking — once you approved the hand-off;
50+# false = each one is asked;
51+# absent = riker's yolo decides (false here).
4252 mmConfigs:
43- milo: agent.milo.yaml
44- homer: agent.homer.yaml
53+ milo:
54+ path: agent.milo.yaml
55+ description: >-
56+ Specifier. Turns a request into a written specification saved in
57+ specs/<slug>.spec.md (requirements, interfaces, acceptance criteria),
58+ and checks code against a specification, criterion by criterion. Does
59+ not write code.
60+ # Milo writes the spec file himself; his prompt keeps him to specs/.
61+ yolo: true
62+ homer:
63+ path: agent.homer.yaml
64+ description: >-
65+ Coder. Implements a specification file from specs/ in this project —
66+ code, tests, build — and reports what he did. Call him only once a
67+ specification exists.
68+ # Coding is homer's job: his edits and commands run without asking once
69+ # you approved the hand-off. Work in a clean git repository.
70+ yolo: true
4571
4672 # Tokens served. 0 = ask llmman once the model is loaded (131,072 for this
4773 # model, its trained maximum, unless LLMMAN_CONTEXT_LENGTH says less).
@@ -51,7 +77,7 @@ contextWindow: 0
5177
5278 sampling:
5379 # Gemma's own metadata recommends temp 1.0 / top_p 0.95 / top_k 64 for
54- # chat. A coordinator must copy commands and file names exactly, so the
80+ # chat. A coordinator must pick agents and name files exactly, so the
5581 # temperature is lowered; top_p / top_k stay as Gemma recommends.
5682 temperature: 0.3
5783 top_p: 0.95
@@ -60,69 +86,58 @@ sampling:
6086 # Riker's replies are short: a tool call, or a summary.
6187 max_tokens: 4096
6288
63- # One tool call at a time: write the brief, THEN ask milo.
89+ # One tool call at a time: milo's spec, THEN homer.
6490 parallel_tool_calls: false
6591
6692 # Gemma 4's template reads `enable_thinking` and defaults it to false
67- # (verified on the GGUF served by llmman). Kept off: routing to a skill
68- # needs no deliberation, and every second counts on a coordinator.
93+ # (verified on the GGUF served by llmman). Kept off: routing needs no
94+ # deliberation, and every second counts on a coordinator.
6995 chat_template_kwargs:
70- enable_thinking: true
96+ enable_thinking: false
7197
72-# Display of the reasoning, if you turn it on above.
7398 showThinking: true
7499
75100 # ─── Timeouts ────────────────────────────────────────────────────────────────
76101
77-# One bash command — that is, one whole `--ask` to milo or homer — may run
78-# this long. homer implementing a spec on a laptop (load ~48 GB, then many
79-# edit/build/test round trips) can take a quarter of an hour.
80-bashTimeout: 30m
81-
82-# Silence tolerated between two tokens. It keeps counting while a bash command
83-# runs, so it must be at least bashTimeout. The price: a genuinely hung
84-# generation of riker is detected only after 30 minutes.
85-watchdogTimeout: 30m
102+# Silence tolerated between two tokens. It also counts while a tool runs —
103+# but during ask_agent every event of the asked agent (text, thought, tool
104+# call) counts as a sign of life, so it only has to cover homer's longest
105+# silence: loading ~48 GB and reading his first prompt. 10 minutes.
106+watchdogTimeout: 10m
86107
87108 # Silence tolerated while the server reads the prompt (and loads the model on
88109 # the first question). Progress reports restart it.
89110 firstTokenTimeout: 3m
90111
112+# Riker runs only short commands itself (ls, cat).
113+bashTimeout: 1m
114+
91115 # ─── Agent loop ──────────────────────────────────────────────────────────────
92116
93-# Round trips per question. The longest path — brief, milo, read, homer,
94-# milo check — is about ten.
117+# Round trips per question. The longest path — milo, homer, milo — is a
118+# handful; room for a second round of fixes.
95119 maxTurns: 20
96120
97-# Characters of a tool result handed back to riker. A spec printed after milo
98-# writes it, or homer's report, fits in 16000.
121+# Characters of a tool result handed back to riker: an agent's answer (a
122+# summary, homer's report, milo's verdict) fits in 16000.
99123 maxOutput: 16000
100124
101-# Lines of a command's output echoed in the terminal; the numbered list of
102-# commands after each answer.
103125 previewLines: 20
104126 displayCommands: true
105127
106128 # ─── Tools ───────────────────────────────────────────────────────────────────
107129
108-# false: every bash call and every file write waits for your answer —
109-# [a] once, [s] this tool for the session, [r] reject, [n] never.
110-# Each hand-off to milo or homer is a bash call, so you approve each one, and
111-# you see the exact command first. Calls MILO makes through `--ask` follow
112-# this setting too: his writes are refused, which keeps him read-only.
113-# HOMER is started with AGENT_YOLO=true by his skill: once you approve the
114-# hand-off, he edits and runs commands without asking — that is his job.
130+# false: every bash call, every file write — and every hand-off (ask_agent) —
131+# waits for your answer: [a] once, [s] this tool for the session, [r] reject,
132+# [n] never. You see the prompt riker wrote for milo or homer before it goes.
115133 # With `mm -p` nobody can answer: run with AGENT_YOLO=true, knowingly.
116134 yolo: false
117135
118-# bash: runs the hand-off scripts in bin/ (specify.sh, implement.sh,
119-# check.sh), which call `"$MM_BIN" --ask …`. A small model copies one short
120-# command reliably; it did NOT copy the raw --ask line (it dropped quotes and
121-# AGENT_YOLO=true on a first run).
136+# bash: `ls specs/`, `cat` a report. Riker never builds or edits.
122137 bashTool: true
123138
124-# read_file / write_file / edit_file: riker writes the BRIEF with write_file
125-# (no quoting problems, unlike a long -p "…"). It never writes code.
139+# read_file (and its siblings): riker reads specs and reports; its prompt
140+# forbids it to write.
126141 editTools: true
127142
128143 # No plan tool, no web, no language server: fewer tools = fewer wrong picks
@@ -130,12 +145,9 @@ editTools: true
130145 planTool: false
131146 fetchContentTool: false
132147
133-# ─── Skills ──────────────────────────────────────────────────────────────────
134-
135-# In the terminal, a relative path is resolved from the directory mm starts
136-# in (your project), so this one is absolute. Adjust it if you move the
137-# repository.
138-skillsDir: /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/skills
148+# No skills: the procedure is in the prompt below. A name that does not
149+# exist = no read_skill tool (and a start-up warning saying so).
150+skillsDir: no-skills-for-riker
139151
140152 # ─── Memory ──────────────────────────────────────────────────────────────────
141153
@@ -160,30 +172,42 @@ context:
160172
161173 acp:
162174 name: riker
163- title: Riker (coordinator)
175+ title: 🐻 Riker (coordinator)
164176
165177 system: |
166- Your name is Riker. You coordinate two specialists:
178+ Your name is Riker. You coordinate two specialists through the `ask_agent`
179+ tool — its description says who does what:
167180 - milo writes specifications and checks code against them;
168181 - homer writes code from a specification.
169- You do not design and you do not write code yourself.
170-
171- HOW YOU WORK
172- You have skills, listed in the `read_skill` tool. For any request to
173- specify, design, build, implement, code, fix or check something, call
174- `read_skill` FIRST with the matching skill, then follow it step by step.
175- The skills tell you the exact commands to run with your `bash` tool.
176-
177- For a simple question (what is in specs/, what milo said), answer it
178- yourself, with a command if you need to read something.
182+ You never design and never write code yourself.
183+
184+ HOW TO BUILD SOMETHING
185+ When the user asks to build, create, add, change or fix something:
186+ 1. Pick a slug: two to four lowercase words joined by "-" (csv-export).
187+ 2. ask_agent milo: "Write the specification for this request and save it
188+ as specs/<slug>.spec.md, then answer with a short summary. Request:
189+ <the user's request, complete, translated to English, with every
190+ constraint they gave>."
191+ 3. Did the user say to do everything at once ("sans t'arrêter", "fais
192+ tout", "go all the way")? Then go straight on to step 4, in the same
193+ answer, without asking. Otherwise show milo's summary and the file
194+ name, ask whether to implement it, and end your answer there.
195+ 4. ask_agent homer: "Implement the specification in specs/<slug>.spec.md."
196+ 5. ask_agent milo: "Check the implementation against
197+ specs/<slug>.spec.md. The files to read are: <the files homer's report
198+ lists>."
199+ 6. Report: the spec file, the files homer changed with his build and test
200+ results, milo's verdict, what is left.
201+ To only specify, stop after step 3. To implement an existing spec, start
202+ at step 4 (`ls specs/` with bash if you do not know the slug).
179203
180204 RULES
181- - Run each hand-off command ONCE. What it prints is the answer: never run
182- it again to get a different one, never answer in milo's or homer's place.
183- - If a command prints `[ask: …]`, the agent failed: tell the user that line.
184- - Never state what a file contains unless a command in this answer read it.
185- - Write only briefs, with `write_file`, under specs/. Never edit code.
186- - Never run git commands unless the user asks.
205+ - Call ask_agent ONCE per step. Its result is that agent's answer: never
206+ call it again to get a different one, never answer in its place.
207+ - If the result starts with "[ask_agent:", the step failed: tell the user
208+ that line and stop.
209+ - Never state what a file contains unless a tool read it in this answer.
210+ - Never write or edit files; never run git commands unless asked.
187211 - Answer in the language the user wrote in — French if they wrote French —
188212 even though milo and homer answer you in English. A few lines: who did
189213 what, which files, what is next.
@@ -1,19 +1,21 @@
1 # Riker — the coordinator. A small, fast model that talks with you and hands1 # Riker — the coordinator. A small, fast model that talks with you and hands
2-# the work to two specialists through `mm --ask` (called from its skills):2+# the work to two specialists with the built-in `ask_agent` tool:
3 #3 #
4-# milo — the specifier (Gemma 4 26B-A4B, reasoning on): turns a request4+# milo — the specifier (Gemma 4 26B-A4B, reasoning on): writes a
5-# into a written specification, then checks code against it.5+# specification into specs/, then checks code against it.
6 # homer — the coder (Qwen3-Coder-Next 80B-A3B): implements a specification.6 # homer — the coder (Qwen3-Coder-Next 80B-A3B): implements a specification.
7 #7 #
8-# The hand-off is FILE-based, in the project's specs/ directory:8+# Riker decides when to call whom, from the descriptions below; no skill is
9-# specs/<slug>.brief.md written by riker (what the user asked)9+# involved. While milo or homer works, the window says so: its header names
10-# specs/<slug>.spec.md written by milo (the specification)10+# the agent and its model ("… asked by mm [Riker (coordinator)]"), and its
11-# code written by homer (from the spec)11+# answers are labelled with its name.
12-# Nobody has to quote a long text on a command line, and every step leaves a
13-# file you can read, edit, or hand to someone else.
14 #12 #
15-# Run it FROM THE PROJECT to work on — milo and homer work in the same13+# The hand-off is FILE-based, in the project's specs/ directory: milo writes
16-# directory as riker's bash:14+# specs/<slug>.spec.md, homer implements it, milo checks the code against it.
15+# Every step leaves a file you can read or edit before the next one.
16+#
17+# Run it FROM THE PROJECT to work on — milo and homer work in the directory
18+# riker was started in:
17 #19 #
18 # cd ~/src/my-project20 # cd ~/src/my-project
19 # mm -config /path/to/mini-mes/riker/agent.riker.yaml21 # mm -config /path/to/mini-mes/riker/agent.riker.yaml
@@ -28,7 +30,8 @@
28 provider: llmman30 provider: llmman
29 31
30 # Gemma 4 E4B (7.5B parameters, ~4B active per token): quick answers, good32 # Gemma 4 E4B (7.5B parameters, ~4B active per token): quick answers, good
31-# enough to pick a skill and run one command. It does not design or code.33+# enough to pick an agent and write it a clear prompt. It does not design or
34+# code.
32 model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M35 model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
33 36
34 # `llmman serve`'s default bind, then the host's llmman seen from a Docker37 # `llmman serve`'s default bind, then the host's llmman seen from a Docker
@@ -36,12 +39,35 @@ model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
36 baseUrl: http://127.0.0.1:17434/v139 baseUrl: http://127.0.0.1:17434/v1
37 fallback: http://host.docker.internal:17434/v140 fallback: http://host.docker.internal:17434/v1
38 41
39-# The agents riker can ask. Names become /ask-milo, /ask-homer (and42+# The agents riker can hand work to. Each one is:
40-# /ask-with-context-…) for YOU to type, and `"$MM_BIN" --ask <name>` for the43+# - a tool target: `ask_agent` (declared because this list is not empty)
41-# scripts in bin/. Paths are relative to this file.44+# shows riker each agent's `description` — that is how it decides;
45+# - a command for YOU: /ask-milo, /ask-homer, /ask-with-context-…
46+# Paths are relative to this file.
47+#
48+# `yolo` decides that agent's own tool calls instead of riker's yolo:
49+# true = they run without asking — once you approved the hand-off;
50+# false = each one is asked;
51+# absent = riker's yolo decides (false here).
42 mmConfigs:52 mmConfigs:
43- milo: agent.milo.yaml53+ milo:
44- homer: agent.homer.yaml54+ path: agent.milo.yaml
55+ description: >-
56+ Specifier. Turns a request into a written specification saved in
57+ specs/<slug>.spec.md (requirements, interfaces, acceptance criteria),
58+ and checks code against a specification, criterion by criterion. Does
59+ not write code.
60+ # Milo writes the spec file himself; his prompt keeps him to specs/.
61+ yolo: true
62+ homer:
63+ path: agent.homer.yaml
64+ description: >-
65+ Coder. Implements a specification file from specs/ in this project —
66+ code, tests, build — and reports what he did. Call him only once a
67+ specification exists.
68+ # Coding is homer's job: his edits and commands run without asking once
69+ # you approved the hand-off. Work in a clean git repository.
70+ yolo: true
45 71
46 # Tokens served. 0 = ask llmman once the model is loaded (131,072 for this72 # Tokens served. 0 = ask llmman once the model is loaded (131,072 for this
47 # model, its trained maximum, unless LLMMAN_CONTEXT_LENGTH says less).73 # model, its trained maximum, unless LLMMAN_CONTEXT_LENGTH says less).
@@ -51,7 +77,7 @@ contextWindow: 0
51 77
52 sampling:78 sampling:
53 # Gemma's own metadata recommends temp 1.0 / top_p 0.95 / top_k 64 for79 # Gemma's own metadata recommends temp 1.0 / top_p 0.95 / top_k 64 for
54- # chat. A coordinator must copy commands and file names exactly, so the80+ # chat. A coordinator must pick agents and name files exactly, so the
55 # temperature is lowered; top_p / top_k stay as Gemma recommends.81 # temperature is lowered; top_p / top_k stay as Gemma recommends.
56 temperature: 0.382 temperature: 0.3
57 top_p: 0.9583 top_p: 0.95
@@ -60,69 +86,58 @@ sampling:
60 # Riker's replies are short: a tool call, or a summary.86 # Riker's replies are short: a tool call, or a summary.
61 max_tokens: 409687 max_tokens: 4096
62 88
63- # One tool call at a time: write the brief, THEN ask milo.89+ # One tool call at a time: milo's spec, THEN homer.
64 parallel_tool_calls: false90 parallel_tool_calls: false
65 91
66 # Gemma 4's template reads `enable_thinking` and defaults it to false92 # Gemma 4's template reads `enable_thinking` and defaults it to false
67- # (verified on the GGUF served by llmman). Kept off: routing to a skill93+ # (verified on the GGUF served by llmman). Kept off: routing needs no
68- # needs no deliberation, and every second counts on a coordinator.94+ # deliberation, and every second counts on a coordinator.
69 chat_template_kwargs:95 chat_template_kwargs:
70- enable_thinking: true96+ enable_thinking: false
71 97
72-# Display of the reasoning, if you turn it on above.
73 showThinking: true98 showThinking: true
74 99
75 # ─── Timeouts ────────────────────────────────────────────────────────────────100 # ─── Timeouts ────────────────────────────────────────────────────────────────
76 101
77-# One bash command — that is, one whole `--ask` to milo or homer — may run102+# Silence tolerated between two tokens. It also counts while a tool runs —
78-# this long. homer implementing a spec on a laptop (load ~48 GB, then many103+# but during ask_agent every event of the asked agent (text, thought, tool
79-# edit/build/test round trips) can take a quarter of an hour.104+# call) counts as a sign of life, so it only has to cover homer's longest
80-bashTimeout: 30m105+# silence: loading ~48 GB and reading his first prompt. 10 minutes.
81-106+watchdogTimeout: 10m
82-# Silence tolerated between two tokens. It keeps counting while a bash command
83-# runs, so it must be at least bashTimeout. The price: a genuinely hung
84-# generation of riker is detected only after 30 minutes.
85-watchdogTimeout: 30m
86 107
87 # Silence tolerated while the server reads the prompt (and loads the model on108 # Silence tolerated while the server reads the prompt (and loads the model on
88 # the first question). Progress reports restart it.109 # the first question). Progress reports restart it.
89 firstTokenTimeout: 3m110 firstTokenTimeout: 3m
90 111
112+# Riker runs only short commands itself (ls, cat).
113+bashTimeout: 1m
114+
91 # ─── Agent loop ──────────────────────────────────────────────────────────────115 # ─── Agent loop ──────────────────────────────────────────────────────────────
92 116
93-# Round trips per question. The longest path — brief, milo, read, homer,117+# Round trips per question. The longest path — milo, homer, milo — is a
94-# milo check — is about ten.118+# handful; room for a second round of fixes.
95 maxTurns: 20119 maxTurns: 20
96 120
97-# Characters of a tool result handed back to riker. A spec printed after milo121+# Characters of a tool result handed back to riker: an agent's answer (a
98-# writes it, or homer's report, fits in 16000.122+# summary, homer's report, milo's verdict) fits in 16000.
99 maxOutput: 16000123 maxOutput: 16000
100 124
101-# Lines of a command's output echoed in the terminal; the numbered list of
102-# commands after each answer.
103 previewLines: 20125 previewLines: 20
104 displayCommands: true126 displayCommands: true
105 127
106 # ─── Tools ───────────────────────────────────────────────────────────────────128 # ─── Tools ───────────────────────────────────────────────────────────────────
107 129
108-# false: every bash call and every file write waits for your answer —130+# false: every bash call, every file write — and every hand-off (ask_agent) —
109-# [a] once, [s] this tool for the session, [r] reject, [n] never.131+# waits for your answer: [a] once, [s] this tool for the session, [r] reject,
110-# Each hand-off to milo or homer is a bash call, so you approve each one, and132+# [n] never. You see the prompt riker wrote for milo or homer before it goes.
111-# you see the exact command first. Calls MILO makes through `--ask` follow
112-# this setting too: his writes are refused, which keeps him read-only.
113-# HOMER is started with AGENT_YOLO=true by his skill: once you approve the
114-# hand-off, he edits and runs commands without asking — that is his job.
115 # With `mm -p` nobody can answer: run with AGENT_YOLO=true, knowingly.133 # With `mm -p` nobody can answer: run with AGENT_YOLO=true, knowingly.
116 yolo: false134 yolo: false
117 135
118-# bash: runs the hand-off scripts in bin/ (specify.sh, implement.sh,136+# bash: `ls specs/`, `cat` a report. Riker never builds or edits.
119-# check.sh), which call `"$MM_BIN" --ask …`. A small model copies one short
120-# command reliably; it did NOT copy the raw --ask line (it dropped quotes and
121-# AGENT_YOLO=true on a first run).
122 bashTool: true137 bashTool: true
123 138
124-# read_file / write_file / edit_file: riker writes the BRIEF with write_file139+# read_file (and its siblings): riker reads specs and reports; its prompt
125-# (no quoting problems, unlike a long -p "…"). It never writes code.140+# forbids it to write.
126 editTools: true141 editTools: true
127 142
128 # No plan tool, no web, no language server: fewer tools = fewer wrong picks143 # No plan tool, no web, no language server: fewer tools = fewer wrong picks
@@ -130,12 +145,9 @@ editTools: true
130 planTool: false145 planTool: false
131 fetchContentTool: false146 fetchContentTool: false
132 147
133-# ─── Skills ──────────────────────────────────────────────────────────────────148+# No skills: the procedure is in the prompt below. A name that does not
134-149+# exist = no read_skill tool (and a start-up warning saying so).
135-# In the terminal, a relative path is resolved from the directory mm starts150+skillsDir: no-skills-for-riker
136-# in (your project), so this one is absolute. Adjust it if you move the
137-# repository.
138-skillsDir: /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/skills
139 151
140 # ─── Memory ──────────────────────────────────────────────────────────────────152 # ─── Memory ──────────────────────────────────────────────────────────────────
141 153
@@ -160,30 +172,42 @@ context:
160 172
161 acp:173 acp:
162 name: riker174 name: riker
163- title: Riker (coordinator)175+ title: 🐻 Riker (coordinator)
164 176
165 system: |177 system: |
166- Your name is Riker. You coordinate two specialists:178+ Your name is Riker. You coordinate two specialists through the `ask_agent`
179+ tool — its description says who does what:
167 - milo writes specifications and checks code against them;180 - milo writes specifications and checks code against them;
168 - homer writes code from a specification.181 - homer writes code from a specification.
169- You do not design and you do not write code yourself.182+ You never design and never write code yourself.
170-183+
171- HOW YOU WORK184+ HOW TO BUILD SOMETHING
172- You have skills, listed in the `read_skill` tool. For any request to185+ When the user asks to build, create, add, change or fix something:
173- specify, design, build, implement, code, fix or check something, call186+ 1. Pick a slug: two to four lowercase words joined by "-" (csv-export).
174- `read_skill` FIRST with the matching skill, then follow it step by step.187+ 2. ask_agent milo: "Write the specification for this request and save it
175- The skills tell you the exact commands to run with your `bash` tool.188+ as specs/<slug>.spec.md, then answer with a short summary. Request:
176-189+ <the user's request, complete, translated to English, with every
177- For a simple question (what is in specs/, what milo said), answer it190+ constraint they gave>."
178- yourself, with a command if you need to read something.191+ 3. Did the user say to do everything at once ("sans t'arrêter", "fais
192+ tout", "go all the way")? Then go straight on to step 4, in the same
193+ answer, without asking. Otherwise show milo's summary and the file
194+ name, ask whether to implement it, and end your answer there.
195+ 4. ask_agent homer: "Implement the specification in specs/<slug>.spec.md."
196+ 5. ask_agent milo: "Check the implementation against
197+ specs/<slug>.spec.md. The files to read are: <the files homer's report
198+ lists>."
199+ 6. Report: the spec file, the files homer changed with his build and test
200+ results, milo's verdict, what is left.
201+ To only specify, stop after step 3. To implement an existing spec, start
202+ at step 4 (`ls specs/` with bash if you do not know the slug).
179 203
180 RULES204 RULES
181- - Run each hand-off command ONCE. What it prints is the answer: never run205+ - Call ask_agent ONCE per step. Its result is that agent's answer: never
182- it again to get a different one, never answer in milo's or homer's place.206+ call it again to get a different one, never answer in its place.
183- - If a command prints `[ask: …]`, the agent failed: tell the user that line.207+ - If the result starts with "[ask_agent:", the step failed: tell the user
184- - Never state what a file contains unless a command in this answer read it.208+ that line and stop.
185- - Write only briefs, with `write_file`, under specs/. Never edit code.209+ - Never state what a file contains unless a tool read it in this answer.
186- - Never run git commands unless the user asks.210+ - Never write or edit files; never run git commands unless asked.
187 - Answer in the language the user wrote in — French if they wrote French —211 - Answer in the language the user wrote in — French if they wrote French —
188 even though milo and homer answer you in English. A few lines: who did212 even though milo and homer answer you in English. A few lines: who did
189 what, which files, what is next.213 what, which files, what is next.
deleted mini-mes/riker/bin/check.sh +0 -24
deleted file mode 100755
@@ -1,24 +0,0 @@
1-#!/usr/bin/env bash
2-# check.sh <slug> — ask milo to check the code against specs/<slug>.spec.md,
3-# using homer's report to find the files; print the verdict
4-# (specs/<slug>.check.md).
5-set -u
6-slug=${1:?usage: check.sh <slug>}
7-: "${MM_BIN:?MM_BIN is not set: run this from an mm bash tool}"
8-spec="specs/$slug.spec.md"
9-report="specs/$slug.report.md"
10-check="specs/$slug.check.md"
11-log="specs/$slug.milo.log"
12-[ -f "$spec" ] || { echo "[riker: $spec not found]"; exit 1; }
13-[ -s "$report" ] || { echo "[riker: $report not found or empty: run the implement skill first]"; exit 1; }
14-
15-"$MM_BIN" --ask milo -p "Check the implementation against $spec. Homer's report, $report, lists the files he created or changed: read it, then read those files." >"$check" 2>"$log"
16-status=$?
17-if [ "$status" -ne 0 ] || [ ! -s "$check" ]; then
18- echo "[milo failed, exit $status]"
19- tail -n 5 "$log"
20- exit 1
21-fi
22-echo "Milo's check ($check):"
23-echo
24-cat "$check"
deleted file mode 100755
@@ -1,24 +0,0 @@
1-#!/usr/bin/env bash
2-# check.sh <slug> — ask milo to check the code against specs/<slug>.spec.md,
3-# using homer's report to find the files; print the verdict
4-# (specs/<slug>.check.md).
5-set -u
6-slug=${1:?usage: check.sh <slug>}
7-: "${MM_BIN:?MM_BIN is not set: run this from an mm bash tool}"
8-spec="specs/$slug.spec.md"
9-report="specs/$slug.report.md"
10-check="specs/$slug.check.md"
11-log="specs/$slug.milo.log"
12-[ -f "$spec" ] || { echo "[riker: $spec not found]"; exit 1; }
13-[ -s "$report" ] || { echo "[riker: $report not found or empty: run the implement skill first]"; exit 1; }
14-
15-"$MM_BIN" --ask milo -p "Check the implementation against $spec. Homer's report, $report, lists the files he created or changed: read it, then read those files." >"$check" 2>"$log"
16-status=$?
17-if [ "$status" -ne 0 ] || [ ! -s "$check" ]; then
18- echo "[milo failed, exit $status]"
19- tail -n 5 "$log"
20- exit 1
21-fi
22-echo "Milo's check ($check):"
23-echo
24-cat "$check"
deleted mini-mes/riker/bin/implement.sh +0 -34
deleted file mode 100755
@@ -1,34 +0,0 @@
1-#!/usr/bin/env bash
2-# implement.sh <slug> — ask homer to implement specs/<slug>.spec.md, then
3-# print his report (specs/<slug>.report.md; all he said: .homer.out).
4-#
5-# AGENT_YOLO=true: homer's file edits and commands are allowed without a
6-# question each time. The user approved THIS command in riker; homer then
7-# works on his own.
8-set -u
9-slug=${1:?usage: implement.sh <slug>}
10-: "${MM_BIN:?MM_BIN is not set: run this from an mm bash tool}"
11-spec="specs/$slug.spec.md"
12-report="specs/$slug.report.md"
13-out="specs/$slug.homer.out"
14-log="specs/$slug.homer.log"
15-[ -f "$spec" ] || { echo "[riker: $spec not found: run the specify skill first]"; exit 1; }
16-
17-AGENT_YOLO=true "$MM_BIN" --ask homer -p "Implement the specification in $spec." >"$out" 2>"$log"
18-status=$?
19-# stdout is everything homer said during his turn; the report is its end.
20-if grep -q '^# Report' "$out"; then
21- sed -n '/^# Report/,$p' "$out" >"$report"
22-else
23- cp "$out" "$report"
24-fi
25-echo "Homer's activity (last lines of $log):"
26-tail -n 5 "$log"
27-echo
28-if [ "$status" -ne 0 ] || [ ! -s "$report" ]; then
29- echo "[homer failed, exit $status]"
30- exit 1
31-fi
32-echo "Homer's report ($report):"
33-echo
34-cat "$report"
deleted file mode 100755
@@ -1,34 +0,0 @@
1-#!/usr/bin/env bash
2-# implement.sh <slug> — ask homer to implement specs/<slug>.spec.md, then
3-# print his report (specs/<slug>.report.md; all he said: .homer.out).
4-#
5-# AGENT_YOLO=true: homer's file edits and commands are allowed without a
6-# question each time. The user approved THIS command in riker; homer then
7-# works on his own.
8-set -u
9-slug=${1:?usage: implement.sh <slug>}
10-: "${MM_BIN:?MM_BIN is not set: run this from an mm bash tool}"
11-spec="specs/$slug.spec.md"
12-report="specs/$slug.report.md"
13-out="specs/$slug.homer.out"
14-log="specs/$slug.homer.log"
15-[ -f "$spec" ] || { echo "[riker: $spec not found: run the specify skill first]"; exit 1; }
16-
17-AGENT_YOLO=true "$MM_BIN" --ask homer -p "Implement the specification in $spec." >"$out" 2>"$log"
18-status=$?
19-# stdout is everything homer said during his turn; the report is its end.
20-if grep -q '^# Report' "$out"; then
21- sed -n '/^# Report/,$p' "$out" >"$report"
22-else
23- cp "$out" "$report"
24-fi
25-echo "Homer's activity (last lines of $log):"
26-tail -n 5 "$log"
27-echo
28-if [ "$status" -ne 0 ] || [ ! -s "$report" ]; then
29- echo "[homer failed, exit $status]"
30- exit 1
31-fi
32-echo "Homer's report ($report):"
33-echo
34-cat "$report"
deleted mini-mes/riker/bin/specify.sh +0 -29
deleted file mode 100755
@@ -1,29 +0,0 @@
1-#!/usr/bin/env bash
2-# specify.sh <slug> — ask milo to turn specs/<slug>.brief.md into
3-# specs/<slug>.spec.md, then print the spec.
4-# Run by riker's bash tool: MM_BIN and MM_CONFIG come from mm.
5-set -u
6-slug=${1:?usage: specify.sh <slug>}
7-: "${MM_BIN:?MM_BIN is not set: run this from an mm bash tool}"
8-brief="specs/$slug.brief.md"
9-spec="specs/$slug.spec.md"
10-out="specs/$slug.milo.out"
11-log="specs/$slug.milo.log"
12-[ -f "$brief" ] || { echo "[riker: $brief not found: write the brief first]"; exit 1; }
13-
14-"$MM_BIN" --ask milo -p "Write the specification for the brief in $brief." >"$out" 2>"$log"
15-status=$?
16-# Anything milo said before the document's first "# " title is not the spec.
17-if grep -q '^# ' "$out"; then
18- sed -n '/^# /,$p' "$out" >"$spec"
19-else
20- cp "$out" "$spec"
21-fi
22-if [ "$status" -ne 0 ] || [ ! -s "$spec" ]; then
23- echo "[milo failed, exit $status]"
24- tail -n 5 "$log"
25- exit 1
26-fi
27-echo "Milo wrote $spec:"
28-echo
29-cat "$spec"
deleted file mode 100755
@@ -1,29 +0,0 @@
1-#!/usr/bin/env bash
2-# specify.sh <slug> — ask milo to turn specs/<slug>.brief.md into
3-# specs/<slug>.spec.md, then print the spec.
4-# Run by riker's bash tool: MM_BIN and MM_CONFIG come from mm.
5-set -u
6-slug=${1:?usage: specify.sh <slug>}
7-: "${MM_BIN:?MM_BIN is not set: run this from an mm bash tool}"
8-brief="specs/$slug.brief.md"
9-spec="specs/$slug.spec.md"
10-out="specs/$slug.milo.out"
11-log="specs/$slug.milo.log"
12-[ -f "$brief" ] || { echo "[riker: $brief not found: write the brief first]"; exit 1; }
13-
14-"$MM_BIN" --ask milo -p "Write the specification for the brief in $brief." >"$out" 2>"$log"
15-status=$?
16-# Anything milo said before the document's first "# " title is not the spec.
17-if grep -q '^# ' "$out"; then
18- sed -n '/^# /,$p' "$out" >"$spec"
19-else
20- cp "$out" "$spec"
21-fi
22-if [ "$status" -ne 0 ] || [ ! -s "$spec" ]; then
23- echo "[milo failed, exit $status]"
24- tail -n 5 "$log"
25- exit 1
26-fi
27-echo "Milo wrote $spec:"
28-echo
29-cat "$spec"
modified mini-mes/riker/go.mod +1 -1
@@ -1,3 +1,3 @@
1-module notes
1+module hello-world-json
22
33 go 1.27
@@ -1,3 +1,3 @@
1-module notes1+module hello-world-json
2 2
3 go 1.273 go 1.27
added mini-mes/riker/handlers.go +28 -0
new file mode 100644
@@ -0,0 +1,28 @@
1+package main
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+)
7+
8+// helloMessage is the value returned by the /hello endpoint.
9+const helloMessage = "Hello World 🌍👋"
10+
11+// newRouter wires the HTTP routes to their handlers.
12+func newRouter() http.Handler {
13+ mux := http.NewServeMux()
14+ mux.HandleFunc("/hello", helloHandler)
15+ return mux
16+}
17+
18+// helloHandler serves GET /hello.
19+func helloHandler(w http.ResponseWriter, r *http.Request) {
20+ if r.Method != http.MethodGet {
21+ http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
22+ return
23+ }
24+
25+ w.Header().Set("Content-Type", "application/json")
26+ w.WriteHeader(http.StatusOK)
27+ json.NewEncoder(w).Encode(map[string]string{"message": helloMessage})
28+}
new file mode 100644
@@ -0,0 +1,28 @@
1+package main
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+)
7+
8+// helloMessage is the value returned by the /hello endpoint.
9+const helloMessage = "Hello World 🌍👋"
10+
11+// newRouter wires the HTTP routes to their handlers.
12+func newRouter() http.Handler {
13+ mux := http.NewServeMux()
14+ mux.HandleFunc("/hello", helloHandler)
15+ return mux
16+}
17+
18+// helloHandler serves GET /hello.
19+func helloHandler(w http.ResponseWriter, r *http.Request) {
20+ if r.Method != http.MethodGet {
21+ http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
22+ return
23+ }
24+
25+ w.Header().Set("Content-Type", "application/json")
26+ w.WriteHeader(http.StatusOK)
27+ json.NewEncoder(w).Encode(map[string]string{"message": helloMessage})
28+}
added mini-mes/riker/handlers_test.go +55 -0
new file mode 100644
@@ -0,0 +1,55 @@
1+package main
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+ "net/http/httptest"
7+ "testing"
8+)
9+
10+// A1: GET /hello returns 200, application/json and the expected body.
11+func TestHelloGet(t *testing.T) {
12+ req := httptest.NewRequest(http.MethodGet, "/hello", nil)
13+ rec := httptest.NewRecorder()
14+ newRouter().ServeHTTP(rec, req)
15+
16+ if rec.Code != http.StatusOK {
17+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
18+ }
19+ if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
20+ t.Fatalf("Content-Type = %q, want %q", ct, "application/json")
21+ }
22+
23+ var body map[string]string
24+ if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
25+ t.Fatalf("body is not valid JSON: %v", err)
26+ }
27+ if got, ok := body["message"]; !ok || got != helloMessage {
28+ t.Fatalf("message = %q (present=%v), want %q", got, ok, helloMessage)
29+ }
30+ if len(body) != 1 {
31+ t.Fatalf("body has %d keys, want 1: %v", len(body), body)
32+ }
33+}
34+
35+// A2: POST /hello returns 405.
36+func TestHelloMethodNotAllowed(t *testing.T) {
37+ req := httptest.NewRequest(http.MethodPost, "/hello", nil)
38+ rec := httptest.NewRecorder()
39+ newRouter().ServeHTTP(rec, req)
40+
41+ if rec.Code != http.StatusMethodNotAllowed {
42+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
43+ }
44+}
45+
46+// A3: an undefined route returns 404.
47+func TestUnknownRoute(t *testing.T) {
48+ req := httptest.NewRequest(http.MethodGet, "/unknown", nil)
49+ rec := httptest.NewRecorder()
50+ newRouter().ServeHTTP(rec, req)
51+
52+ if rec.Code != http.StatusNotFound {
53+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
54+ }
55+}
new file mode 100644
@@ -0,0 +1,55 @@
1+package main
2+
3+import (
4+ "encoding/json"
5+ "net/http"
6+ "net/http/httptest"
7+ "testing"
8+)
9+
10+// A1: GET /hello returns 200, application/json and the expected body.
11+func TestHelloGet(t *testing.T) {
12+ req := httptest.NewRequest(http.MethodGet, "/hello", nil)
13+ rec := httptest.NewRecorder()
14+ newRouter().ServeHTTP(rec, req)
15+
16+ if rec.Code != http.StatusOK {
17+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
18+ }
19+ if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
20+ t.Fatalf("Content-Type = %q, want %q", ct, "application/json")
21+ }
22+
23+ var body map[string]string
24+ if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
25+ t.Fatalf("body is not valid JSON: %v", err)
26+ }
27+ if got, ok := body["message"]; !ok || got != helloMessage {
28+ t.Fatalf("message = %q (present=%v), want %q", got, ok, helloMessage)
29+ }
30+ if len(body) != 1 {
31+ t.Fatalf("body has %d keys, want 1: %v", len(body), body)
32+ }
33+}
34+
35+// A2: POST /hello returns 405.
36+func TestHelloMethodNotAllowed(t *testing.T) {
37+ req := httptest.NewRequest(http.MethodPost, "/hello", nil)
38+ rec := httptest.NewRecorder()
39+ newRouter().ServeHTTP(rec, req)
40+
41+ if rec.Code != http.StatusMethodNotAllowed {
42+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
43+ }
44+}
45+
46+// A3: an undefined route returns 404.
47+func TestUnknownRoute(t *testing.T) {
48+ req := httptest.NewRequest(http.MethodGet, "/unknown", nil)
49+ rec := httptest.NewRecorder()
50+ newRouter().ServeHTTP(rec, req)
51+
52+ if rec.Code != http.StatusNotFound {
53+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
54+ }
55+}
added mini-mes/riker/main.go +30 -0
new file mode 100644
@@ -0,0 +1,30 @@
1+package main
2+
3+import (
4+ "log"
5+ "net/http"
6+ "os"
7+ "strconv"
8+)
9+
10+// defaultPort is the TCP port the service listens on when PORT is unset.
11+const defaultPort = 7070
12+
13+func main() {
14+ addr := ":" + strconv.Itoa(resolvePort())
15+ log.Printf("listening on %s", addr)
16+ if err := http.ListenAndServe(addr, newRouter()); err != nil {
17+ log.Fatal(err)
18+ }
19+}
20+
21+// resolvePort returns the port to listen on: the PORT environment variable
22+// when set to a valid integer, otherwise defaultPort.
23+func resolvePort() int {
24+ if p := os.Getenv("PORT"); p != "" {
25+ if n, err := strconv.Atoi(p); err == nil {
26+ return n
27+ }
28+ }
29+ return defaultPort
30+}
new file mode 100644
@@ -0,0 +1,30 @@
1+package main
2+
3+import (
4+ "log"
5+ "net/http"
6+ "os"
7+ "strconv"
8+)
9+
10+// defaultPort is the TCP port the service listens on when PORT is unset.
11+const defaultPort = 7070
12+
13+func main() {
14+ addr := ":" + strconv.Itoa(resolvePort())
15+ log.Printf("listening on %s", addr)
16+ if err := http.ListenAndServe(addr, newRouter()); err != nil {
17+ log.Fatal(err)
18+ }
19+}
20+
21+// resolvePort returns the port to listen on: the PORT environment variable
22+// when set to a valid integer, otherwise defaultPort.
23+func resolvePort() int {
24+ if p := os.Getenv("PORT"); p != "" {
25+ if n, err := strconv.Atoi(p); err == nil {
26+ return n
27+ }
28+ }
29+ return defaultPort
30+}
added mini-mes/riker/main_test.go +29 -0
new file mode 100644
@@ -0,0 +1,29 @@
1+package main
2+
3+import (
4+ "testing"
5+)
6+
7+// R1: without a PORT environment variable, the port is the new default (7070).
8+func TestResolvePortDefault(t *testing.T) {
9+ t.Setenv("PORT", "")
10+ if got := resolvePort(); got != 7070 {
11+ t.Fatalf("resolvePort() = %d, want %d", got, 7070)
12+ }
13+}
14+
15+// R2: when PORT is a valid integer it takes precedence over the default.
16+func TestResolvePortEnvPrecedence(t *testing.T) {
17+ t.Setenv("PORT", "9090")
18+ if got := resolvePort(); got != 9090 {
19+ t.Fatalf("resolvePort() = %d, want %d", got, 9090)
20+ }
21+}
22+
23+// Edge case: when PORT is not a valid integer, fall back to the default.
24+func TestResolvePortInvalidEnv(t *testing.T) {
25+ t.Setenv("PORT", "not-a-number")
26+ if got := resolvePort(); got != 7070 {
27+ t.Fatalf("resolvePort() = %d, want %d", got, 7070)
28+ }
29+}
new file mode 100644
@@ -0,0 +1,29 @@
1+package main
2+
3+import (
4+ "testing"
5+)
6+
7+// R1: without a PORT environment variable, the port is the new default (7070).
8+func TestResolvePortDefault(t *testing.T) {
9+ t.Setenv("PORT", "")
10+ if got := resolvePort(); got != 7070 {
11+ t.Fatalf("resolvePort() = %d, want %d", got, 7070)
12+ }
13+}
14+
15+// R2: when PORT is a valid integer it takes precedence over the default.
16+func TestResolvePortEnvPrecedence(t *testing.T) {
17+ t.Setenv("PORT", "9090")
18+ if got := resolvePort(); got != 9090 {
19+ t.Fatalf("resolvePort() = %d, want %d", got, 9090)
20+ }
21+}
22+
23+// Edge case: when PORT is not a valid integer, fall back to the default.
24+func TestResolvePortInvalidEnv(t *testing.T) {
25+ t.Setenv("PORT", "not-a-number")
26+ if got := resolvePort(); got != 7070 {
27+ t.Fatalf("resolvePort() = %d, want %d", got, 7070)
28+ }
29+}
modified mini-mes/riker/skills-homer/implement-spec/SKILL.md +2 -0
@@ -14,6 +14,8 @@ keywords: implement, specification, spec, implémenter, code, requirements, buil
1414
1515 3. **Implement, one requirement at a time**, in the spec's order: edit the code, write the test that proves it, run the narrowest build/test command. Fix until it passes, then go on. Never leave the project in a state that does not build.
1616
17+ In a Go project, when the `lsp` tool is there: before changing an existing function or type, `references` on it to see every caller; after editing a `.go` file, `diagnostics` on it and fix what gopls reports before building; for a rename, `rename` rather than editing by hand.
18+
1719 4. **Check the acceptance criteria**: run each one that is a command (the spec's examples) and compare with the expected output.
1820
1921 5. **Run the full build and the full test suite** at the end. Format the code with the language's formatter if the project uses one (`gofmt`, `prettier`, `ruff format`…).
@@ -14,6 +14,8 @@ keywords: implement, specification, spec, implémenter, code, requirements, buil
14 14
15 3. **Implement, one requirement at a time**, in the spec's order: edit the code, write the test that proves it, run the narrowest build/test command. Fix until it passes, then go on. Never leave the project in a state that does not build.15 3. **Implement, one requirement at a time**, in the spec's order: edit the code, write the test that proves it, run the narrowest build/test command. Fix until it passes, then go on. Never leave the project in a state that does not build.
16 16
17+ In a Go project, when the `lsp` tool is there: before changing an existing function or type, `references` on it to see every caller; after editing a `.go` file, `diagnostics` on it and fix what gopls reports before building; for a rename, `rename` rather than editing by hand.
18+
17 4. **Check the acceptance criteria**: run each one that is a command (the spec's examples) and compare with the expected output.19 4. **Check the acceptance criteria**: run each one that is a command (the spec's examples) and compare with the expected output.
18 20
19 5. **Run the full build and the full test suite** at the end. Format the code with the language's formatter if the project uses one (`gofmt`, `prettier`, `ruff format`…).21 5. **Run the full build and the full test suite** at the end. Format the code with the language's formatter if the project uses one (`gofmt`, `prettier`, `ruff format`…).
added mini-mes/riker/skills-homer/lsp-navigation/SKILL.md +45 -0
new file mode 100644
@@ -0,0 +1,45 @@
1+---
2+name: lsp-navigation
3+description: Navigate and change code with the `lsp` tool — find where a symbol is defined, who uses it, what its type is, what the compiler reports, and rename it everywhere.
4+keywords: lsp, definition, references, usages, callers, hover, type, signature, symbols, outline, diagnostics, errors, warnings, rename, refactor, language server
5+---
6+
7+# Navigate code with the `lsp` tool
8+
9+## When to use it
10+
11+The `lsp` tool asks the project's language server what the compiler knows. Prefer it to `grep` and to reading whole files when the question is about **symbols**: where something is declared, who uses it, what type it has, what is wrong in a file, or how to rename it safely. Its description lists the servers available and the file extensions they cover; for other files, fall back to `bash` and `read_file`.
12+
13+## How to point at a symbol
14+
15+Give the **identifier** as written in the code, in `symbol`: the tool finds its first occurrence in the file. Add `line` (1-based) when the same name appears several times and you want a particular one. This is the reliable way; never guess a column.
16+
17+Only when you have an exact position — from `read_file` with `numbered=true` — may you send `line` and `column` instead (both 1-based, the column pointing inside the identifier).
18+
19+## The actions
20+
21+| You want to know… | Call |
22+| --- | --- |
23+| Where a symbol is declared | `{"action":"definition","path":"main.go","symbol":"hello"}` |
24+| Every place that uses it | `{"action":"references","path":"main.go","symbol":"hello"}` |
25+| Its signature, type and documentation | `{"action":"hover","path":"main.go","symbol":"hello"}` |
26+| What a file contains (outline) | `{"action":"symbols","path":"main.go"}` |
27+| What the compiler reports in a file | `{"action":"diagnostics","path":"main.go"}` |
28+| Rename it everywhere, files rewritten | `{"action":"rename","path":"main.go","symbol":"hello","new_name":"greet"}` |
29+| The occurrence on one line only | add `"line": 9` to any of the calls above |
30+
31+Answers come as `path:line:column <source line>`, one per location. You rarely need to open the file afterwards.
32+
33+## Procedure for a change
34+
35+1. `symbols` on the file to find the symbol, or `read_file numbered=true` if you already know roughly where it is.
36+2. `references` on the symbol to see every file the change touches. Say how many there are.
37+3. Edit with `edit_file` (or `rename` when the change is a rename — it rewrites all the files at once).
38+4. `diagnostics` on each edited file to see what the compiler reports, then fix what it says.
39+5. Only then run the tests with `bash`.
40+
41+## Rules
42+
43+- A `[lsp: …]` answer is information, not a crash: read it. "no language server for" means the extension is not covered — use `bash`. "not found in" means the identifier is not spelled as in the code — read the file and copy it. "past the end of line" means you miscounted — use `symbol` instead.
44+- `diagnostics` right after an edit may say "as last published" or "no diagnostics received": the server is still indexing. Ask again once.
45+- `rename` writes files. It NEEDS `new_name`, the new identifier; without it the tool answers with the exact call to make — make it, do not fall back to `edit_file`. Say what it changed, and never rename to a name the file already uses for something else.
new file mode 100644
@@ -0,0 +1,45 @@
1+---
2+name: lsp-navigation
3+description: Navigate and change code with the `lsp` tool — find where a symbol is defined, who uses it, what its type is, what the compiler reports, and rename it everywhere.
4+keywords: lsp, definition, references, usages, callers, hover, type, signature, symbols, outline, diagnostics, errors, warnings, rename, refactor, language server
5+---
6+
7+# Navigate code with the `lsp` tool
8+
9+## When to use it
10+
11+The `lsp` tool asks the project's language server what the compiler knows. Prefer it to `grep` and to reading whole files when the question is about **symbols**: where something is declared, who uses it, what type it has, what is wrong in a file, or how to rename it safely. Its description lists the servers available and the file extensions they cover; for other files, fall back to `bash` and `read_file`.
12+
13+## How to point at a symbol
14+
15+Give the **identifier** as written in the code, in `symbol`: the tool finds its first occurrence in the file. Add `line` (1-based) when the same name appears several times and you want a particular one. This is the reliable way; never guess a column.
16+
17+Only when you have an exact position — from `read_file` with `numbered=true` — may you send `line` and `column` instead (both 1-based, the column pointing inside the identifier).
18+
19+## The actions
20+
21+| You want to know… | Call |
22+| --- | --- |
23+| Where a symbol is declared | `{"action":"definition","path":"main.go","symbol":"hello"}` |
24+| Every place that uses it | `{"action":"references","path":"main.go","symbol":"hello"}` |
25+| Its signature, type and documentation | `{"action":"hover","path":"main.go","symbol":"hello"}` |
26+| What a file contains (outline) | `{"action":"symbols","path":"main.go"}` |
27+| What the compiler reports in a file | `{"action":"diagnostics","path":"main.go"}` |
28+| Rename it everywhere, files rewritten | `{"action":"rename","path":"main.go","symbol":"hello","new_name":"greet"}` |
29+| The occurrence on one line only | add `"line": 9` to any of the calls above |
30+
31+Answers come as `path:line:column <source line>`, one per location. You rarely need to open the file afterwards.
32+
33+## Procedure for a change
34+
35+1. `symbols` on the file to find the symbol, or `read_file numbered=true` if you already know roughly where it is.
36+2. `references` on the symbol to see every file the change touches. Say how many there are.
37+3. Edit with `edit_file` (or `rename` when the change is a rename — it rewrites all the files at once).
38+4. `diagnostics` on each edited file to see what the compiler reports, then fix what it says.
39+5. Only then run the tests with `bash`.
40+
41+## Rules
42+
43+- A `[lsp: …]` answer is information, not a crash: read it. "no language server for" means the extension is not covered — use `bash`. "not found in" means the identifier is not spelled as in the code — read the file and copy it. "past the end of line" means you miscounted — use `symbol` instead.
44+- `diagnostics` right after an edit may say "as last published" or "no diagnostics received": the server is still indexing. Ask again once.
45+- `rename` writes files. It NEEDS `new_name`, the new identifier; without it the tool answers with the exact call to make — make it, do not fall back to `edit_file`. Say what it changed, and never rename to a name the file already uses for something else.
deleted mini-mes/riker/skills/build-feature/SKILL.md +0 -30
deleted file mode 100644
@@ -1,30 +0,0 @@
1----
2-name: build-feature
3-description: Build something end to end with the team — Milo specifies it, Homer implements it, Milo checks the result. Use when the user asks to build, create or add something without asking for a spec first.
4-keywords: construire, créer, ajoute, fais, build, create, feature, fonctionnalité
5----
6-
7-# Build a feature with Milo and Homer
8-
9-## When to use it
10-
11-The user asks to build, create or add something ("crée un CLI qui…", "add an endpoint that…"). For "only specify" use `specify`; for "implement this spec" use `implement`.
12-
13-## Steps
14-
15-Follow the three skills below in order. Load each one with `read_skill` when you reach it, and do exactly what it says. Use the same slug from start to end.
16-
17-1. **`specify`** — Milo writes `specs/<slug>.spec.md`.
18- Then STOP and show the user the summary the skill asks for, unless the user said to go all the way ("sans t'arrêter", "do everything", "go ahead and build it"). A wrong spec costs less to fix now than after Homer's work.
19-
20-2. **`implement`** — when the user approves (or asked to go all the way), Homer implements the spec.
21-
22-3. **`check-against-spec`** — Milo checks Homer's code against the spec.
23-
24-## Final report
25-
26-In the user's language, short:
27-- the spec file and its goal;
28-- the files Homer created or changed, with his build and test results;
29-- Milo's verdict: criteria met / not met;
30-- what is left, if anything, and the next step you propose.
deleted file mode 100644
@@ -1,30 +0,0 @@
1----
2-name: build-feature
3-description: Build something end to end with the team — Milo specifies it, Homer implements it, Milo checks the result. Use when the user asks to build, create or add something without asking for a spec first.
4-keywords: construire, créer, ajoute, fais, build, create, feature, fonctionnalité
5----
6-
7-# Build a feature with Milo and Homer
8-
9-## When to use it
10-
11-The user asks to build, create or add something ("crée un CLI qui…", "add an endpoint that…"). For "only specify" use `specify`; for "implement this spec" use `implement`.
12-
13-## Steps
14-
15-Follow the three skills below in order. Load each one with `read_skill` when you reach it, and do exactly what it says. Use the same slug from start to end.
16-
17-1. **`specify`** — Milo writes `specs/<slug>.spec.md`.
18- Then STOP and show the user the summary the skill asks for, unless the user said to go all the way ("sans t'arrêter", "do everything", "go ahead and build it"). A wrong spec costs less to fix now than after Homer's work.
19-
20-2. **`implement`** — when the user approves (or asked to go all the way), Homer implements the spec.
21-
22-3. **`check-against-spec`** — Milo checks Homer's code against the spec.
23-
24-## Final report
25-
26-In the user's language, short:
27-- the spec file and its goal;
28-- the files Homer created or changed, with his build and test results;
29-- Milo's verdict: criteria met / not met;
30-- what is left, if anything, and the next step you propose.
deleted mini-mes/riker/skills/check-against-spec/SKILL.md +0 -21
deleted file mode 100644
@@ -1,21 +0,0 @@
1----
2-name: check-against-spec
3-description: Have Milo check the code Homer wrote against its specification, acceptance criterion by acceptance criterion.
4-keywords: vérifier, check, contrôle, conforme, valider, verify, acceptance, review
5----
6-
7-# Check the code against the spec with Milo
8-
9-## When to use it
10-
11-After `implement`, or when the user asks whether the code matches the specification. Step 3 of the `build-feature` skill.
12-
13-## Steps
14-
15-1. **Ask Milo.** Call your `bash` tool with exactly this command, replacing only `<slug>`:
16-
17- bash /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/bin/check.sh <slug>
18-
19- Milo reads the spec, Homer's report `specs/<slug>.report.md`, and the files it lists. Run it ONCE. It prints one line per acceptance criterion: met, not met, or cannot tell, saved in `specs/<slug>.check.md`. If it prints `[milo failed …]` or `[riker: …]`, tell the user those lines.
20-
21-2. **Report.** Tell the user how many criteria are met, and list the others with Milo's reason. If some are "not met", offer to send them back to Homer: then use the `implement` skill again — Homer reads the spec and the code, and finishes the work.
deleted file mode 100644
@@ -1,21 +0,0 @@
1----
2-name: check-against-spec
3-description: Have Milo check the code Homer wrote against its specification, acceptance criterion by acceptance criterion.
4-keywords: vérifier, check, contrôle, conforme, valider, verify, acceptance, review
5----
6-
7-# Check the code against the spec with Milo
8-
9-## When to use it
10-
11-After `implement`, or when the user asks whether the code matches the specification. Step 3 of the `build-feature` skill.
12-
13-## Steps
14-
15-1. **Ask Milo.** Call your `bash` tool with exactly this command, replacing only `<slug>`:
16-
17- bash /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/bin/check.sh <slug>
18-
19- Milo reads the spec, Homer's report `specs/<slug>.report.md`, and the files it lists. Run it ONCE. It prints one line per acceptance criterion: met, not met, or cannot tell, saved in `specs/<slug>.check.md`. If it prints `[milo failed …]` or `[riker: …]`, tell the user those lines.
20-
21-2. **Report.** Tell the user how many criteria are met, and list the others with Milo's reason. If some are "not met", offer to send them back to Homer: then use the `implement` skill again — Homer reads the spec and the code, and finishes the work.
deleted mini-mes/riker/skills/implement/SKILL.md +0 -26
deleted file mode 100644
@@ -1,26 +0,0 @@
1----
2-name: implement
3-description: Have Homer implement an existing specification file (specs/<slug>.spec.md) — he writes the code and tests in the project and reports what he did.
4-keywords: implémenter, implement, code, coder, développer, programmer, réalise, homer
5----
6-
7-# Implement with Homer
8-
9-## When to use it
10-
11-A specification exists in `specs/` and the user wants it implemented ("vas-y", "implement it", "code it"). Also step 2 of the `build-feature` skill. If there is no spec yet, use the `specify` skill first.
12-
13-## Steps
14-
15-1. **Find the spec.** If you do not know the slug, call your `bash` tool with: `ls specs/*.spec.md`. If several exist and the user did not say which, ask.
16-
17-2. **Ask Homer.** Call your `bash` tool with exactly this command, replacing only `<slug>`:
18-
19- bash /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/bin/implement.sh <slug>
20-
21- - The script lets Homer edit files and run commands without asking at each step: the user approves this one command, then Homer works.
22- - It can take a long time (up to 30 minutes). Run it ONCE. Never run it again because it was slow.
23- - It prints the last lines of Homer's activity, then his report, saved in `specs/<slug>.report.md`.
24- - If it prints `[homer failed …]`, tell the user the lines it printed.
25-
26-3. **Report.** Tell the user, in their language: the files Homer created or changed, the commands he ran and their results (build, tests), what he chose where the spec was silent, and what is left. Quote his results; do not add claims of your own. Offer to have Milo check the code (the `check-against-spec` skill).
deleted file mode 100644
@@ -1,26 +0,0 @@
1----
2-name: implement
3-description: Have Homer implement an existing specification file (specs/<slug>.spec.md) — he writes the code and tests in the project and reports what he did.
4-keywords: implémenter, implement, code, coder, développer, programmer, réalise, homer
5----
6-
7-# Implement with Homer
8-
9-## When to use it
10-
11-A specification exists in `specs/` and the user wants it implemented ("vas-y", "implement it", "code it"). Also step 2 of the `build-feature` skill. If there is no spec yet, use the `specify` skill first.
12-
13-## Steps
14-
15-1. **Find the spec.** If you do not know the slug, call your `bash` tool with: `ls specs/*.spec.md`. If several exist and the user did not say which, ask.
16-
17-2. **Ask Homer.** Call your `bash` tool with exactly this command, replacing only `<slug>`:
18-
19- bash /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/bin/implement.sh <slug>
20-
21- - The script lets Homer edit files and run commands without asking at each step: the user approves this one command, then Homer works.
22- - It can take a long time (up to 30 minutes). Run it ONCE. Never run it again because it was slow.
23- - It prints the last lines of Homer's activity, then his report, saved in `specs/<slug>.report.md`.
24- - If it prints `[homer failed …]`, tell the user the lines it printed.
25-
26-3. **Report.** Tell the user, in their language: the files Homer created or changed, the commands he ran and their results (build, tests), what he chose where the spec was silent, and what is left. Quote his results; do not add claims of your own. Offer to have Milo check the code (the `check-against-spec` skill).
deleted mini-mes/riker/skills/specify/SKILL.md +0 -37
deleted file mode 100644
@@ -1,37 +0,0 @@
1----
2-name: specify
3-description: Have Milo write a specification for a feature, a change or a program the user describes — a brief goes in specs/, Milo's spec comes back as specs/<slug>.spec.md.
4-keywords: spécifier, spécification, spec, specify, design, conçois, cahier des charges, besoin
5----
6-
7-# Specify with Milo
8-
9-## When to use it
10-
11-The user describes something to build or change and wants it specified, designed, or "thought through" first. Also step 1 of the `build-feature` skill.
12-
13-## Steps
14-
15-1. **Pick a slug**: two to four lowercase words joined by `-`, from the request (`csv-export`, `login-rate-limit`). Use the same slug for every file of this request.
16-
17-2. **Write the brief** with your `write_file` tool, path `specs/<slug>.brief.md`, in English:
18-
19- # Brief: <title>
20- ## Request
21- <the user's request, complete, in their words translated to English>
22- ## Constraints
23- <language, libraries, files to change, anything the user said; "none stated" otherwise>
24- ## Files to read
25- <paths of existing files that matter, one per line; "none" for a new project>
26-
27- To find the files to list, you may first call your `bash` tool with: `ls; find . -maxdepth 2 -type f -not -path './.git/*' -not -path './.mm/*' | head -50`
28-
29-3. **Ask Milo.** Call your `bash` tool with exactly this command, replacing only `<slug>`:
30-
31- bash /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/bin/specify.sh <slug>
32-
33- - It can take several minutes. Run it ONCE.
34- - It prints the specification Milo wrote, saved in `specs/<slug>.spec.md`.
35- - If it prints `[milo failed …]`, tell the user the lines it printed. Do not write a spec yourself.
36-
37-4. **Report and stop.** Tell the user, in their language: the file `specs/<slug>.spec.md`, the goal in one sentence, the number of requirements and acceptance criteria, and Milo's open questions with their defaults. Ask whether to implement it (the `implement` skill) or change something first. Do not implement it in the same answer unless the user already asked you to build it.
deleted file mode 100644
@@ -1,37 +0,0 @@
1----
2-name: specify
3-description: Have Milo write a specification for a feature, a change or a program the user describes — a brief goes in specs/, Milo's spec comes back as specs/<slug>.spec.md.
4-keywords: spécifier, spécification, spec, specify, design, conçois, cahier des charges, besoin
5----
6-
7-# Specify with Milo
8-
9-## When to use it
10-
11-The user describes something to build or change and wants it specified, designed, or "thought through" first. Also step 1 of the `build-feature` skill.
12-
13-## Steps
14-
15-1. **Pick a slug**: two to four lowercase words joined by `-`, from the request (`csv-export`, `login-rate-limit`). Use the same slug for every file of this request.
16-
17-2. **Write the brief** with your `write_file` tool, path `specs/<slug>.brief.md`, in English:
18-
19- # Brief: <title>
20- ## Request
21- <the user's request, complete, in their words translated to English>
22- ## Constraints
23- <language, libraries, files to change, anything the user said; "none stated" otherwise>
24- ## Files to read
25- <paths of existing files that matter, one per line; "none" for a new project>
26-
27- To find the files to list, you may first call your `bash` tool with: `ls; find . -maxdepth 2 -type f -not -path './.git/*' -not -path './.mm/*' | head -50`
28-
29-3. **Ask Milo.** Call your `bash` tool with exactly this command, replacing only `<slug>`:
30-
31- bash /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/bin/specify.sh <slug>
32-
33- - It can take several minutes. Run it ONCE.
34- - It prints the specification Milo wrote, saved in `specs/<slug>.spec.md`.
35- - If it prints `[milo failed …]`, tell the user the lines it printed. Do not write a spec yourself.
36-
37-4. **Report and stop.** Tell the user, in their language: the file `specs/<slug>.spec.md`, the goal in one sentence, the number of requirements and acceptance criteria, and Milo's open questions with their defaults. Ask whether to implement it (the `implement` skill) or change something first. Do not implement it in the same answer unless the user already asked you to build it.
added mini-mes/riker/specs/change-port-7070.spec.md +29 -0
new file mode 100644
@@ -0,0 +1,29 @@
1+# Change Port to 7070
2+
3+## Goal
4+Modify the Go web service to use port 7070 as its default listening port instead of 8080.
5+
6+## Context
7+The service is currently implemented in `main.go` and uses a constant `defaultPort` set to `8080`. It also allows overriding the port via the `PORT` environment variable.
8+
9+## Requirements
10+R1: Change the `defaultPort` constant from `8080` to `7070`.
11+R2: Ensure the service still respects the `PORT` environment variable if it is provided.
12+
13+## Interfaces
14+No changes to external interfaces (HTTP routes or types) are required, only the default listening address.
15+
16+## Edge cases and errors
17+- If the `PORT` environment variable is provided, it should take precedence over the new default (7070).
18+- If the `PORT` environment variable is not a valid integer, the service should fallback to the new default (7070).
19+
20+## Acceptance criteria
21+A1: Given the service is started without a `PORT` environment variable, when it is running, then it should be listening on port 7070.
22+A2: Given the service is started with `PORT=9090`, when it is running, then it should be listening on port 9090.
23+
24+## Out of scope
25+- Changing the router or any HTTP handlers.
26+- Adding support for other protocols.
27+
28+## Open questions
29+None.
new file mode 100644
@@ -0,0 +1,29 @@
1+# Change Port to 7070
2+
3+## Goal
4+Modify the Go web service to use port 7070 as its default listening port instead of 8080.
5+
6+## Context
7+The service is currently implemented in `main.go` and uses a constant `defaultPort` set to `8080`. It also allows overriding the port via the `PORT` environment variable.
8+
9+## Requirements
10+R1: Change the `defaultPort` constant from `8080` to `7070`.
11+R2: Ensure the service still respects the `PORT` environment variable if it is provided.
12+
13+## Interfaces
14+No changes to external interfaces (HTTP routes or types) are required, only the default listening address.
15+
16+## Edge cases and errors
17+- If the `PORT` environment variable is provided, it should take precedence over the new default (7070).
18+- If the `PORT` environment variable is not a valid integer, the service should fallback to the new default (7070).
19+
20+## Acceptance criteria
21+A1: Given the service is started without a `PORT` environment variable, when it is running, then it should be listening on port 7070.
22+A2: Given the service is started with `PORT=9090`, when it is running, then it should be listening on port 9090.
23+
24+## Out of scope
25+- Changing the router or any HTTP handlers.
26+- Adding support for other protocols.
27+
28+## Open questions
29+None.
deleted mini-mes/riker/specs/crud-notes-webapp.brief.md +0 -7
deleted file mode 100644
@@ -1,7 +0,0 @@
1-# Brief: CRUD Webapp for Notes
2-## Request
3-Create a CRUD webapp to handle a list of notes.
4-## Constraints
5-Backend must be written in Go.
6-## Files to read
7-none
deleted file mode 100644
@@ -1,7 +0,0 @@
1-# Brief: CRUD Webapp for Notes
2-## Request
3-Create a CRUD webapp to handle a list of notes.
4-## Constraints
5-Backend must be written in Go.
6-## Files to read
7-none
deleted mini-mes/riker/specs/crud-notes-webapp.homer.log +0 -9
deleted file mode 100644
@@ -1,9 +0,0 @@
1-🗣 asking homer (llmman/hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M)…
2-🛠️ homer · read_skill: implement-spec
3-🛠️ homer · read_file: specs/crud-notes-webapp.spec.md
4-🛠️ homer · bash: ls -la && echo "---" && find . -type f -not -path './.git/*' | head -50
5-🛠️ homer · bash: go version && echo "---GOPATH---" && go env GOPATH GOMODCACHE GOPROXY && echo "---which uuid libs---" && ls $(go env GOMODCACHE) 2>/dev/null | head
6-🛠️ homer · bash: ls $(go env GOMODCACHE)/github.com/google/ 2>/dev/null | grep -i uuid; echo "---"; ls $(go env GOMODCACHE)/github.com/ 2>/dev/null | head -40
7-🛠️ homer · write_file: go.mod (22 bytes)
8-🛠️ homer · bash: cd /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker && ls -la && echo "---GO---" && go version && echo "---uuid available?---" && ls /Users/k33g/go/pkg/mod/github.com/google/ 2>/dev/null | grep uuid
9-[ask: interrupted]
deleted file mode 100644
@@ -1,9 +0,0 @@
1-🗣 asking homer (llmman/hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M)…
2-🛠️ homer · read_skill: implement-spec
3-🛠️ homer · read_file: specs/crud-notes-webapp.spec.md
4-🛠️ homer · bash: ls -la && echo "---" && find . -type f -not -path './.git/*' | head -50
5-🛠️ homer · bash: go version && echo "---GOPATH---" && go env GOPATH GOMODCACHE GOPROXY && echo "---which uuid libs---" && ls $(go env GOMODCACHE) 2>/dev/null | head
6-🛠️ homer · bash: ls $(go env GOMODCACHE)/github.com/google/ 2>/dev/null | grep -i uuid; echo "---"; ls $(go env GOMODCACHE)/github.com/ 2>/dev/null | head -40
7-🛠️ homer · write_file: go.mod (22 bytes)
8-🛠️ homer · bash: cd /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker && ls -la && echo "---GO---" && go version && echo "---uuid available?---" && ls /Users/k33g/go/pkg/mod/github.com/google/ 2>/dev/null | grep uuid
9-[ask: interrupted]
deleted mini-mes/riker/specs/crud-notes-webapp.homer.out +0 -6
deleted file mode 100644
@@ -1,6 +0,0 @@
1-Now let me look at the project structure.
2-
3-This is a new project. Let me check the Go environment.
4-
5-This is a new project. Go 1.27 is available (supports `http.ServeMux` routing patterns and `PathValue`), and `github.com/google/uuid` is in the module cache. Let me set up the project.
6-
deleted file mode 100644
@@ -1,6 +0,0 @@
1-Now let me look at the project structure.
2-
3-This is a new project. Let me check the Go environment.
4-
5-This is a new project. Go 1.27 is available (supports `http.ServeMux` routing patterns and `PathValue`), and `github.com/google/uuid` is in the module cache. Let me set up the project.
6-
deleted mini-mes/riker/specs/crud-notes-webapp.milo.log +0 -2
deleted file mode 100644
@@ -1,2 +0,0 @@
1-🗣 asking milo (llmman/hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M)…
2-🛠️ milo · read_file: specs/crud-notes-webapp.brief.md
deleted file mode 100644
@@ -1,2 +0,0 @@
1-🗣 asking milo (llmman/hf.co/unsloth/gemma-4-26B-A4B-it-GGUF:Q4_K_M)…
2-🛠️ milo · read_file: specs/crud-notes-webapp.brief.md
deleted mini-mes/riker/specs/crud-notes-webapp.milo.out +0 -67
deleted file mode 100644
@@ -1,67 +0,0 @@
1-# CRUD Notes Webapp Specification
2-
3-## Goal
4-Implement a RESTful web application in Go that allows users to perform CRUD operations on a collection of notes. This backend will serve as the data provider for a notes management system.
5-
6-## Context
7-This is a new project. There is no existing code.
8-
9-## Requirements
10-R1: The system shall provide an endpoint to create a new note.
11-R2: The system shall provide an endpoint to retrieve a list of all existing notes.
12-R3: The system shall provide an endpoint to retrieve a single note by its unique ID.
13-R4: The system shall provide an endpoint to update an existing note.
14-R5: The system shall provide an endpoint to delete an existing note.
15-R6: The system shall store notes in memory during the application lifecycle.
16-R7: The backend shall be implemented in Go.
17-
18-## Interfaces
19-
20-### Data Model
21-`Note` struct:
22-```go
23-type Note struct {
24- ID string `json:"id"`
25- Title string `json:"title"`
26- Content string `json:"content"`
27- CreatedAt time.Time `json:"created_at"`
28- UpdatedAt time.Time `json:"updated_at"`
29-}
30-```
31-
32-### HTTP API
33-- `POST /notes`: Create a note.
34- - Request Body: `{"title": string, "content": string}`
35- - Response: `201 Created` with the created `Note` object.
36-- `GET /notes`: List all notes.
37- - Response: `200 OK` with a JSON array of `Note` objects.
38-- `GET /notes/{id}`: Get a specific note.
39- - Response: `200 OK` with the `Note` object or `404 Not Found`.
40-- `PUT /notes/{id}`: Update an existing note.
41- - Request Body: `{"title": string, "content": string}`
42- - Response: `200 OK` with the updated `Note` object or `404 Not Found`.
43-- `DELETE /notes/{id}`: Delete a note.
44- - Response: `204 No Content` or `404 Not Found`.
45-
46-## Edge cases and errors
47-- E1: Request body contains invalid JSON: Return `400 Bad Request`.
48-- E2: Note creation request missing required fields (title/content): Return `400 Bad Request`.
49-- E3: Requesting or updating a non-existent ID: Return `404 Not Found`.
50-- E4: Deleting a non-existent ID: Return `404 Not Found`.
51-
52-## Acceptance criteria
53-A1: Given a running server, When `POST /notes` is called with `{"title": "Meeting", "content": "Discuss specs"}`, Then the response is `201 Created` and the body contains a note with a generated ID.
54-A2: Given notes exist, When `GET /notes` is called, Then the response is `200 OK` and the body contains a list including those notes.
55-A3: Given a note exists, When `GET /notes/{id}` is called with that ID, Then the response is `200 OK` and the body matches the note.
56-A4: Given a note exists, When `PUT /notes/{id}` is called with `{"title": "New Title", "content": "New Content"}`, Then the response is `200 OK` and the note reflects the changes.
57-A5: Given a note exists, When `DELETE /notes/{id}` is called, Then the response is `204 No Content` and subsequent `GET` requests for that ID return `404 Not Found`.
58-
59-## Out of scope
60-- Frontend/UI implementation.
61-- Persistent database storage (e.g., PostgreSQL, SQLite).
62-- User authentication or authorization.
63-- Advanced filtering or search functionality.
64-
65-## Open questions
66-- Q1: Should IDs be UUIDs or incremental integers? Default: UUIDs (strings).
67-- Q2: Should the API support partial updates (PATCH)? Default: No, only full updates via PUT.
deleted file mode 100644
@@ -1,67 +0,0 @@
1-# CRUD Notes Webapp Specification
2-
3-## Goal
4-Implement a RESTful web application in Go that allows users to perform CRUD operations on a collection of notes. This backend will serve as the data provider for a notes management system.
5-
6-## Context
7-This is a new project. There is no existing code.
8-
9-## Requirements
10-R1: The system shall provide an endpoint to create a new note.
11-R2: The system shall provide an endpoint to retrieve a list of all existing notes.
12-R3: The system shall provide an endpoint to retrieve a single note by its unique ID.
13-R4: The system shall provide an endpoint to update an existing note.
14-R5: The system shall provide an endpoint to delete an existing note.
15-R6: The system shall store notes in memory during the application lifecycle.
16-R7: The backend shall be implemented in Go.
17-
18-## Interfaces
19-
20-### Data Model
21-`Note` struct:
22-```go
23-type Note struct {
24- ID string `json:"id"`
25- Title string `json:"title"`
26- Content string `json:"content"`
27- CreatedAt time.Time `json:"created_at"`
28- UpdatedAt time.Time `json:"updated_at"`
29-}
30-```
31-
32-### HTTP API
33-- `POST /notes`: Create a note.
34- - Request Body: `{"title": string, "content": string}`
35- - Response: `201 Created` with the created `Note` object.
36-- `GET /notes`: List all notes.
37- - Response: `200 OK` with a JSON array of `Note` objects.
38-- `GET /notes/{id}`: Get a specific note.
39- - Response: `200 OK` with the `Note` object or `404 Not Found`.
40-- `PUT /notes/{id}`: Update an existing note.
41- - Request Body: `{"title": string, "content": string}`
42- - Response: `200 OK` with the updated `Note` object or `404 Not Found`.
43-- `DELETE /notes/{id}`: Delete a note.
44- - Response: `204 No Content` or `404 Not Found`.
45-
46-## Edge cases and errors
47-- E1: Request body contains invalid JSON: Return `400 Bad Request`.
48-- E2: Note creation request missing required fields (title/content): Return `400 Bad Request`.
49-- E3: Requesting or updating a non-existent ID: Return `404 Not Found`.
50-- E4: Deleting a non-existent ID: Return `404 Not Found`.
51-
52-## Acceptance criteria
53-A1: Given a running server, When `POST /notes` is called with `{"title": "Meeting", "content": "Discuss specs"}`, Then the response is `201 Created` and the body contains a note with a generated ID.
54-A2: Given notes exist, When `GET /notes` is called, Then the response is `200 OK` and the body contains a list including those notes.
55-A3: Given a note exists, When `GET /notes/{id}` is called with that ID, Then the response is `200 OK` and the body matches the note.
56-A4: Given a note exists, When `PUT /notes/{id}` is called with `{"title": "New Title", "content": "New Content"}`, Then the response is `200 OK` and the note reflects the changes.
57-A5: Given a note exists, When `DELETE /notes/{id}` is called, Then the response is `204 No Content` and subsequent `GET` requests for that ID return `404 Not Found`.
58-
59-## Out of scope
60-- Frontend/UI implementation.
61-- Persistent database storage (e.g., PostgreSQL, SQLite).
62-- User authentication or authorization.
63-- Advanced filtering or search functionality.
64-
65-## Open questions
66-- Q1: Should IDs be UUIDs or incremental integers? Default: UUIDs (strings).
67-- Q2: Should the API support partial updates (PATCH)? Default: No, only full updates via PUT.
deleted mini-mes/riker/specs/crud-notes-webapp.spec.md +0 -67
deleted file mode 100644
@@ -1,67 +0,0 @@
1-# CRUD Notes Webapp Specification
2-
3-## Goal
4-Implement a RESTful web application in Go that allows users to perform CRUD operations on a collection of notes. This backend will serve as the data provider for a notes management system.
5-
6-## Context
7-This is a new project. There is no existing code.
8-
9-## Requirements
10-R1: The system shall provide an endpoint to create a new note.
11-R2: The system shall provide an endpoint to retrieve a list of all existing notes.
12-R3: The system shall provide an endpoint to retrieve a single note by its unique ID.
13-R4: The system shall provide an endpoint to update an existing note.
14-R5: The system shall provide an endpoint to delete an existing note.
15-R6: The system shall store notes in memory during the application lifecycle.
16-R7: The backend shall be implemented in Go.
17-
18-## Interfaces
19-
20-### Data Model
21-`Note` struct:
22-```go
23-type Note struct {
24- ID string `json:"id"`
25- Title string `json:"title"`
26- Content string `json:"content"`
27- CreatedAt time.Time `json:"created_at"`
28- UpdatedAt time.Time `json:"updated_at"`
29-}
30-```
31-
32-### HTTP API
33-- `POST /notes`: Create a note.
34- - Request Body: `{"title": string, "content": string}`
35- - Response: `201 Created` with the created `Note` object.
36-- `GET /notes`: List all notes.
37- - Response: `200 OK` with a JSON array of `Note` objects.
38-- `GET /notes/{id}`: Get a specific note.
39- - Response: `200 OK` with the `Note` object or `404 Not Found`.
40-- `PUT /notes/{id}`: Update an existing note.
41- - Request Body: `{"title": string, "content": string}`
42- - Response: `200 OK` with the updated `Note` object or `404 Not Found`.
43-- `DELETE /notes/{id}`: Delete a note.
44- - Response: `204 No Content` or `404 Not Found`.
45-
46-## Edge cases and errors
47-- E1: Request body contains invalid JSON: Return `400 Bad Request`.
48-- E2: Note creation request missing required fields (title/content): Return `400 Bad Request`.
49-- E3: Requesting or updating a non-existent ID: Return `404 Not Found`.
50-- E4: Deleting a non-existent ID: Return `404 Not Found`.
51-
52-## Acceptance criteria
53-A1: Given a running server, When `POST /notes` is called with `{"title": "Meeting", "content": "Discuss specs"}`, Then the response is `201 Created` and the body contains a note with a generated ID.
54-A2: Given notes exist, When `GET /notes` is called, Then the response is `200 OK` and the body contains a list including those notes.
55-A3: Given a note exists, When `GET /notes/{id}` is called with that ID, Then the response is `200 OK` and the body matches the note.
56-A4: Given a note exists, When `PUT /notes/{id}` is called with `{"title": "New Title", "content": "New Content"}`, Then the response is `200 OK` and the note reflects the changes.
57-A5: Given a note exists, When `DELETE /notes/{id}` is called, Then the response is `204 No Content` and subsequent `GET` requests for that ID return `404 Not Found`.
58-
59-## Out of scope
60-- Frontend/UI implementation.
61-- Persistent database storage (e.g., PostgreSQL, SQLite).
62-- User authentication or authorization.
63-- Advanced filtering or search functionality.
64-
65-## Open questions
66-- Q1: Should IDs be UUIDs or incremental integers? Default: UUIDs (strings).
67-- Q2: Should the API support partial updates (PATCH)? Default: No, only full updates via PUT.
deleted file mode 100644
@@ -1,67 +0,0 @@
1-# CRUD Notes Webapp Specification
2-
3-## Goal
4-Implement a RESTful web application in Go that allows users to perform CRUD operations on a collection of notes. This backend will serve as the data provider for a notes management system.
5-
6-## Context
7-This is a new project. There is no existing code.
8-
9-## Requirements
10-R1: The system shall provide an endpoint to create a new note.
11-R2: The system shall provide an endpoint to retrieve a list of all existing notes.
12-R3: The system shall provide an endpoint to retrieve a single note by its unique ID.
13-R4: The system shall provide an endpoint to update an existing note.
14-R5: The system shall provide an endpoint to delete an existing note.
15-R6: The system shall store notes in memory during the application lifecycle.
16-R7: The backend shall be implemented in Go.
17-
18-## Interfaces
19-
20-### Data Model
21-`Note` struct:
22-```go
23-type Note struct {
24- ID string `json:"id"`
25- Title string `json:"title"`
26- Content string `json:"content"`
27- CreatedAt time.Time `json:"created_at"`
28- UpdatedAt time.Time `json:"updated_at"`
29-}
30-```
31-
32-### HTTP API
33-- `POST /notes`: Create a note.
34- - Request Body: `{"title": string, "content": string}`
35- - Response: `201 Created` with the created `Note` object.
36-- `GET /notes`: List all notes.
37- - Response: `200 OK` with a JSON array of `Note` objects.
38-- `GET /notes/{id}`: Get a specific note.
39- - Response: `200 OK` with the `Note` object or `404 Not Found`.
40-- `PUT /notes/{id}`: Update an existing note.
41- - Request Body: `{"title": string, "content": string}`
42- - Response: `200 OK` with the updated `Note` object or `404 Not Found`.
43-- `DELETE /notes/{id}`: Delete a note.
44- - Response: `204 No Content` or `404 Not Found`.
45-
46-## Edge cases and errors
47-- E1: Request body contains invalid JSON: Return `400 Bad Request`.
48-- E2: Note creation request missing required fields (title/content): Return `400 Bad Request`.
49-- E3: Requesting or updating a non-existent ID: Return `404 Not Found`.
50-- E4: Deleting a non-existent ID: Return `404 Not Found`.
51-
52-## Acceptance criteria
53-A1: Given a running server, When `POST /notes` is called with `{"title": "Meeting", "content": "Discuss specs"}`, Then the response is `201 Created` and the body contains a note with a generated ID.
54-A2: Given notes exist, When `GET /notes` is called, Then the response is `200 OK` and the body contains a list including those notes.
55-A3: Given a note exists, When `GET /notes/{id}` is called with that ID, Then the response is `200 OK` and the body matches the note.
56-A4: Given a note exists, When `PUT /notes/{id}` is called with `{"title": "New Title", "content": "New Content"}`, Then the response is `200 OK` and the note reflects the changes.
57-A5: Given a note exists, When `DELETE /notes/{id}` is called, Then the response is `204 No Content` and subsequent `GET` requests for that ID return `404 Not Found`.
58-
59-## Out of scope
60-- Frontend/UI implementation.
61-- Persistent database storage (e.g., PostgreSQL, SQLite).
62-- User authentication or authorization.
63-- Advanced filtering or search functionality.
64-
65-## Open questions
66-- Q1: Should IDs be UUIDs or incremental integers? Default: UUIDs (strings).
67-- Q2: Should the API support partial updates (PATCH)? Default: No, only full updates via PUT.
added mini-mes/riker/specs/hello-world-json.spec.md +41 -0
new file mode 100644
@@ -0,0 +1,41 @@
1+# Hello World JSON Web Service
2+
3+## Goal
4+Create a minimal Go web service that serves a JSON response containing a "Hello World" message decorated with emojis.
5+
6+## Context
7+This is a new project with no existing codebase.
8+
9+## Requirements
10+1. The service must be written in Go.
11+2. The service must listen on a TCP port (default 8080).
12+3. The service must provide a single HTTP GET endpoint at `/hello`.
13+4. The response for `/hello` must have an HTTP 200 OK status.
14+5. The response body must be a valid JSON object.
15+6. The JSON object must contain a single key `message` with the value "Hello World 🌍👋".
16+7. The `Content-Type` header of the response must be `application/json`.
17+
18+## Interfaces
19+### HTTP Routes
20+- `GET /hello`
21+ - Response Body: `{"message": "Hello World 🌍👋"}`
22+ - Headers: `Content-Type: application/json`
23+
24+## Edge cases and errors
25+1. Any HTTP method other than `GET` on `/hello` must return `405 Method Not Allowed`.
26+2. Any request to an undefined route must return `404 Not Found`.
27+
28+## Acceptance criteria
29+1. A1: Given the server is running, When I perform `curl -i http://localhost:8080/hello`, Then I receive a `200 OK` status and the body `{"message": "Hello World 🌍👋"}` with `Content-Type: application/json`.
30+2. A2: Given the server is running, When I perform `curl -i -X POST http://localhost:8080/hello`, Then I receive a `405 Method Not Allowed` status.
31+3. A3: Given the server is running, When I perform `curl -i http://localhost:8080/unknown`, Then I receive a `404 Not Found` status.
32+
33+## Out of scope
34+- Adding more endpoints.
35+- Using external web frameworks (e.g., Gin, Echo); use standard `net/http` unless otherwise requested.
36+- Persistent storage or databases.
37+- Dockerization or CI/CD pipelines.
38+
39+## Open questions
40+- Which emojis to use? (Default: 🌍👋)
41+- Which web framework to use? (Default: standard library `net/http`)
new file mode 100644
@@ -0,0 +1,41 @@
1+# Hello World JSON Web Service
2+
3+## Goal
4+Create a minimal Go web service that serves a JSON response containing a "Hello World" message decorated with emojis.
5+
6+## Context
7+This is a new project with no existing codebase.
8+
9+## Requirements
10+1. The service must be written in Go.
11+2. The service must listen on a TCP port (default 8080).
12+3. The service must provide a single HTTP GET endpoint at `/hello`.
13+4. The response for `/hello` must have an HTTP 200 OK status.
14+5. The response body must be a valid JSON object.
15+6. The JSON object must contain a single key `message` with the value "Hello World 🌍👋".
16+7. The `Content-Type` header of the response must be `application/json`.
17+
18+## Interfaces
19+### HTTP Routes
20+- `GET /hello`
21+ - Response Body: `{"message": "Hello World 🌍👋"}`
22+ - Headers: `Content-Type: application/json`
23+
24+## Edge cases and errors
25+1. Any HTTP method other than `GET` on `/hello` must return `405 Method Not Allowed`.
26+2. Any request to an undefined route must return `404 Not Found`.
27+
28+## Acceptance criteria
29+1. A1: Given the server is running, When I perform `curl -i http://localhost:8080/hello`, Then I receive a `200 OK` status and the body `{"message": "Hello World 🌍👋"}` with `Content-Type: application/json`.
30+2. A2: Given the server is running, When I perform `curl -i -X POST http://localhost:8080/hello`, Then I receive a `405 Method Not Allowed` status.
31+3. A3: Given the server is running, When I perform `curl -i http://localhost:8080/unknown`, Then I receive a `404 Not Found` status.
32+
33+## Out of scope
34+- Adding more endpoints.
35+- Using external web frameworks (e.g., Gin, Echo); use standard `net/http` unless otherwise requested.
36+- Persistent storage or databases.
37+- Dockerization or CI/CD pipelines.
38+
39+## Open questions
40+- Which emojis to use? (Default: 🌍👋)
41+- Which web framework to use? (Default: standard library `net/http`)