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 @@ | ||
| 2 | 2 | "workbench.iconTheme": "material-icon-theme", |
| 3 | 3 | "workbench.colorTheme": "GitHub Light Colorblind (Beta)", |
| 4 | 4 | "editor.fontSize": 14, |
| 5 | - "terminal.integrated.fontSize": 14, | |
| 5 | + "terminal.integrated.fontSize": 16, | |
| 6 | 6 | "editor.insertSpaces": true, |
| 7 | 7 | "editor.tabSize": 4, |
| 8 | 8 | "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 | ||
| 6 | 6 | |
| 7 | 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 | 10 | | `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), a full agent with its tools. | |
| 11 | 11 | | `agent.lucy.yaml` | **Lucy** (`hf.co/Menlo/Lucy-gguf:Q4_K_M`), a full agent with its tools. | |
| 12 | 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 | 13 | | `skills/bg-jobs` | Copy of the general skill (Bob's prompt refers to it). | |
| 17 | 14 | |
| 18 | 15 | In `agent.llmman.yaml`: |
| 19 | 16 | |
| 20 | 17 | ```yaml |
| 21 | 18 | 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. | |
| 25 | 29 | ``` |
| 26 | 30 | |
| 27 | 31 | ## Prerequisites |
| @@ -86,58 +90,56 @@ printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, P | ||
| 86 | 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 | 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 | 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 skills | |
| 95 | +## 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 | | |
| 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) | | |
| 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 | 107 | ```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. | |
| 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 | 118 | ## Time settings |
| 119 | 119 | |
| 120 | 120 | A small local model can take a minute to answer, so `agent.llmman.yaml` raises two limits: |
| 121 | 121 | |
| 122 | 122 | ```yaml |
| 123 | -watchdogTimeout: 3m # the watchdog keeps counting while a command runs (ticket 57) | |
| 124 | -bashTimeout: 3m # how long one bash command may run (default 30s) | |
| 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 | |
| 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 | 129 | ## 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`. | |
| 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. | |
| 134 | 134 | - **No sessions for the asked agents**: they run with `AGENT_SESSIONS=off`, so your `/sessions` lists Bob's only. |
| 135 | 135 | - **Limited depth**: Bob may ask Jan, Jan may ask another agent, but that one cannot ask anybody (`MM_ASK_DEPTH`). |
| 136 | 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 | 139 | ## Going further |
| 139 | 140 | |
| 140 | 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 | 143 | - [How to ask another agent from a skill or a script](../docs/en/how-to/ask-another-agent-from-a-skill.md) |
| 142 | 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 | 145 | - [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 | ```yaml | 17 | ```yaml |
| 21 | mmConfigs: | 18 | mmConfigs: |
| 22 | - jan: agent.jan-nano.yaml | 19 | + jan: |
| 23 | - lucy: agent.lucy.yaml | 20 | + path: agent.jan-nano.yaml |
| 24 | - reviewer: agent.lucy-reviewer.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. | ||
| 25 | ``` | 29 | ``` |
| 26 | 30 | ||
| 27 | ## Prerequisites | 31 | ## 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 skills | 95 | +## 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 | ```text | 107 | ```text |
| 104 | -📖 read_skill: second-opinion | 108 | +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 settings | 118 | ## 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 | ```yaml | 122 | ```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 know | 129 | ## 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 further | 139 | ## 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 | ||
| 6 | 6 | |
| 7 | 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 | 10 | | `agent.jan-nano.yaml` | **Jan** (`hf.co/Menlo/Jan-nano-gguf:Q4_K_M`), agent complet avec ses outils. | |
| 11 | 11 | | `agent.lucy.yaml` | **Lucy** (`hf.co/Menlo/Lucy-gguf:Q4_K_M`), agent complet avec ses outils. | |
| 12 | 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 | 13 | | `skills/bg-jobs` | Copie de la skill générale (le prompt de Bob y fait référence). | |
| 17 | 14 | |
| 18 | 15 | Dans `agent.llmman.yaml` : |
| 19 | 16 | |
| 20 | 17 | ```yaml |
| 21 | 18 | 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. | |
| 25 | 29 | ``` |
| 26 | 30 | |
| 27 | 31 | ## Prérequis |
| @@ -86,58 +90,56 @@ printf 'User: My name is Philippe and I like Go.\nAssistant: Nice to meet you, P | ||
| 86 | 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 | 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 | 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 skills | |
| 95 | +## 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 | | |
| 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) | | |
| 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 | 107 | ```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. | |
| 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 | 118 | ## Réglages de temps |
| 119 | 119 | |
| 120 | 120 | Un petit modèle local peut mettre une minute à répondre. `agent.llmman.yaml` relève donc deux délais : |
| 121 | 121 | |
| 122 | 122 | ```yaml |
| 123 | -watchdogTimeout: 3m # le watchdog continue de compter pendant qu'une commande tourne (ticket 57) | |
| 124 | -bashTimeout: 3m # durée maximale d'une commande bash (30s par défaut) | |
| 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 | |
| 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 | 129 | ## 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`. | |
| 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. | |
| 134 | 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 | 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 | 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 | 139 | ## Pour aller plus loin |
| 139 | 140 | |
| 140 | 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 | 143 | - [Comment interroger un autre agent depuis une skill ou un script](../docs/fr/how-to/ask-another-agent-from-a-skill.md) |
| 142 | 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 | 145 | - [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 | ```yaml | 17 | ```yaml |
| 21 | mmConfigs: | 18 | mmConfigs: |
| 22 | - jan: agent.jan-nano.yaml | 19 | + jan: |
| 23 | - lucy: agent.lucy.yaml | 20 | + path: agent.jan-nano.yaml |
| 24 | - reviewer: agent.lucy-reviewer.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. | ||
| 25 | ``` | 29 | ``` |
| 26 | 30 | ||
| 27 | ## Prérequis | 31 | ## 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 skills | 95 | +## 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 | ```text | 107 | ```text |
| 104 | -📖 read_skill: second-opinion | 108 | +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 temps | 118 | ## 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 | ```yaml | 122 | ```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 à savoir | 129 | ## 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 loin | 139 | ## 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 | ||
| 17 | 17 | # From a Docker sandbox, the daemon runs on the host. |
| 18 | 18 | fallback: http://host.docker.internal:17434/v1 |
| 19 | 19 | |
| 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 | |
| 24 | 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 | 35 | 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. | |
| 32 | 51 | |
| 33 | 52 | acp: |
| 34 | 53 | name: bob |
| @@ -143,6 +162,14 @@ system: | | ||
| 143 | 162 | you invent. Match the request against that list, call `read_skill` FIRST, |
| 144 | 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 | 173 | FILE EDITING |
| 147 | 174 | You have three tools for files: `read_file`, `edit_file` and `write_file`. |
| 148 | 175 | They are how a file gets read and changed here: each change is exact, |
| @@ -201,10 +228,10 @@ sampling: | ||
| 201 | 228 | # Silence allowed between two tokens once the answer has started. |
| 202 | 229 | watchdogTimeout: 3m |
| 203 | 230 | |
| 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.) | |
| 208 | 235 | bashTimeout: 3m |
| 209 | 236 | |
| 210 | 237 | # 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/v1 | 18 | fallback: http://host.docker.internal:17434/v1 |
| 19 | 19 | ||
| 20 | -# Other mm agents this one can ask. Each name becomes /ask-<name> and | 20 | +# Other mm agents this one can ask. Two ways: |
| 21 | -# /ask-with-context-<name> (also `/ask <name> <prompt>` in the terminal and the | 21 | +# - 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 are | 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 | ||
| 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.yaml | 36 | + jan: |
| 27 | - lucy: agent.lucy.yaml | 37 | + path: agent.jan-nano.yaml |
| 28 | - # Lucy's model with no tools and a reviewer prompt: what the second-opinion | 38 | + 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, loads | 39 | + lucy: |
| 30 | - # skills) when it is only asked for an opinion. | 40 | + path: agent.lucy.yaml |
| 31 | - reviewer: agent.lucy-reviewer.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. | ||
| 32 | 51 | ||
| 33 | acp: | 52 | acp: |
| 34 | name: bob | 53 | 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 EDITING | 173 | 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: 3m | 229 | watchdogTimeout: 3m |
| 203 | 230 | ||
| 204 | -# How long one bash command may run (default 30s). The demo skills call jan and | 231 | +# 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 local | 232 | +# 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: 3m | 235 | 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) | ||
| 14 | 14 | - [Run mini-me on llmman](how-to/run-with-llmman.md) — `provider: llmman`, the demo config in `demo-llmman/` |
| 15 | 15 | - [Run mini-me on Ollama](how-to/run-with-ollama.md) — `provider: ollama`, the demo config in `demo-ollama/` |
| 16 | 16 | - [Ask another mm agent from your session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<name>`, `/ask-with-context-<name>` |
| 17 | -- [Ask another agent from a skill or a script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, the demo skills | |
| 17 | +- [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 | 19 | - [Use mini-me from an editor (ACP)](how-to/use-from-an-editor.md) |
| 19 | 20 | - [Turn a model's reasoning off](how-to/turn-reasoning-off.md) — `chat_template_kwargs` or `reasoning_effort` in `sampling`, per server |
| 20 | 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 | 62 | - [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 | 63 | - [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 | 64 | - [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 | |
| 65 | 66 | - Package diagram (draw.io source): [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 66 | 67 | |
| 67 | 68 | ### 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 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` | ||
| 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 server | 20 | - [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 limits | 62 | - [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 alive | 63 | - [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 decides | 64 | - [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 |
| 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) Explanation | 68 | ### MCP (Model Context Protocol) Explanation |
modified
docs/en/explanation/asking-another-agent.md +7 -4 | @@ -2,7 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 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 | 7 | ## Why it is designed this way |
| 8 | 8 | |
| @@ -10,7 +10,7 @@ | ||
| 10 | 10 | |
| 11 | 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 | 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 | 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 | 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 | 31 | ## Rejected alternatives |
| 30 | 32 | |
| @@ -32,6 +34,7 @@ | ||
| 32 | 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 | 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 | 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 | 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 | 40 | ## 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 way | 7 | ## 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 alternatives | 31 | ## 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 rest | 40 | ## How it relates to the rest |
modified
docs/en/how-to/ask-another-agent-from-a-skill.md +24 -5 | @@ -1,6 +1,6 @@ | ||
| 1 | 1 | # 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 | 5 | ## 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 | 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; | |
| 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). | |
| 33 | 52 | |
| 34 | 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 | |
| @@ -1,6 +1,6 @@ | |||
| 1 | # How to ask another agent from a skill or a script | 1 | # 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 | ## Steps | 5 | ## 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 @@ | ||
| 33 | 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 | 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 | 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 | 38 | ## `context` keys |
| 39 | 39 | |
| @@ -124,7 +124,7 @@ Reported as `[config error: <path>: <message>]`, exit code 1. | ||
| 124 | 124 | | `bashTimeout must be > 0` | zero or negative | |
| 125 | 125 | | `mmConfigs: "Bob" is not a valid name (…)` | a name with uppercase letters, spaces or other characters, or starting with `-` | |
| 126 | 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 | 128 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Reported by the engine, as `[engine error: …]`. | |
| 129 | 129 | |
| 130 | 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 …`). |
| @@ -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` keys | 38 | ## `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 | ||
| 200 | 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 | 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 | 225 | ## Loop detection |
| 204 | 226 | |
| 205 | 227 | 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 detection | 225 | ## 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: | ||
| 14 | 14 | - [Faire tourner mini-me sur llmman](how-to/run-with-llmman.md) — `provider: llmman`, la config de démo dans `demo-llmman/` |
| 15 | 15 | - [Faire tourner mini-me sur Ollama](how-to/run-with-ollama.md) — `provider: ollama`, la config de démo dans `demo-ollama/` |
| 16 | 16 | - [Interroger un autre agent mm depuis votre session](how-to/ask-another-agent.md) — `mmConfigs`, `/ask-<nom>`, `/ask-with-context-<nom>` |
| 17 | -- [Interroger un autre agent depuis une skill ou un script](how-to/ask-another-agent-from-a-skill.md) — `"$MM_BIN" --ask <agent>`, `--ask-with-context`, les skills de démo | |
| 17 | +- [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 | 19 | - [Utiliser mini-me depuis un éditeur (ACP)](how-to/use-from-an-editor.md) |
| 19 | 20 | - [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 | 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 | 62 | - [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 | 63 | - [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 | 64 | - [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 | |
| 65 | 66 | - Diagramme des paquets (source draw.io) : [`docs/diagrams/packages.drawio`](../diagrams/packages.drawio) |
| 66 | 67 | |
| 67 | 68 | ### 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é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` | ||
| 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 serveur | 20 | - [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 connues | 62 | - [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 vie | 63 | - [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écide | 64 | - [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 |
| 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 @@ | ||
| 2 | 2 | |
| 3 | 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 | 7 | ## Pourquoi c'est conçu ainsi |
| 8 | 8 | |
| @@ -10,7 +10,7 @@ | ||
| 10 | 10 | |
| 11 | 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 | 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 | 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 | 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 | 31 | ## Alternatives écartées |
| 30 | 32 | |
| @@ -32,6 +34,7 @@ | ||
| 32 | 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 | 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 | 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 | 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 | 40 | ## 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 ainsi | 7 | ## 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ées | 31 | ## 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 reste | 40 | ## Lien avec le reste |
modified
docs/fr/how-to/ask-another-agent-from-a-skill.md +24 -5 | @@ -1,6 +1,6 @@ | ||
| 1 | 1 | # 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 | 5 | ## É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 | 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 ; | |
| 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). | |
| 33 | 52 | |
| 34 | 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 | |
| @@ -1,6 +1,6 @@ | |||
| 1 | # Comment interroger un autre agent depuis une skill ou un script | 1 | # 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 | ## Étapes | 5 | ## É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 @@ | ||
| 33 | 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 | 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 | 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 | 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 | 124 | | `bashTimeout must be > 0` | zéro ou négative | |
| 125 | 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 | 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 | 128 | | `unknown provider "x" (known: dmr, llamacpp, llmman, ollama)` | Signalée par le moteur, sous la forme `[engine error: …]`. | |
| 129 | 129 | |
| 130 | 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 …`). |
| @@ -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 | ||
| 200 | 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 | 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 | 225 | ## Détection de boucle |
| 204 | 226 | |
| 205 | 227 | 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 boucle | 225 | ## 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) { | ||
| 28 | 28 | t.Helper() |
| 29 | 29 | saved := config.Cfg |
| 30 | 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 | 32 | command.SetAgents([]string{"bob"}) |
| 33 | 33 | } |
| 34 | 34 | |
| @@ -28,7 +28,7 @@ func withBob(t *testing.T) { | |||
| 28 | t.Helper() | 28 | t.Helper() |
| 29 | saved := config.Cfg | 29 | 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 ( | ||
| 7 | 7 | |
| 8 | 8 | "mm/internal/ask" |
| 9 | 9 | "mm/internal/command" |
| 10 | - "mm/internal/config" | |
| 11 | - "mm/internal/consent" | |
| 12 | - "mm/internal/spinner" | |
| 13 | 10 | "mm/internal/store" |
| 14 | - "mm/internal/ui" | |
| 15 | 11 | |
| 16 | 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 | 38 | done := make(chan result, 1) |
| 43 | 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 | 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 | 88 | dimln(fmt.Sprintf("🗣 %s's answer is in the history for your next question", req.Agent)) |
| 93 | 89 | 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 } | |
| @@ -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 messages | 89 | 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) { | ||
| 39 | 39 | saved := config.Cfg |
| 40 | 40 | t.Cleanup(func() { config.Cfg = saved }) |
| 41 | 41 | 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 | 43 | sig, in := make(chan os.Signal, 1), make(chan string, 1) |
| 44 | 44 | return asking{pool: r, cwd: t.TempDir(), keep: store.NewKeeper(t.TempDir()), sigCh: sig, inputCh: in}, sig, in |
| 45 | 45 | } |
| @@ -39,7 +39,7 @@ func lineAsk(t *testing.T, r fakeRunner) (asking, chan os.Signal, chan string) { | |||
| 39 | saved := config.Cfg | 39 | saved := config.Cfg |
| 40 | t.Cleanup(func() { config.Cfg = saved }) | 40 | t.Cleanup(func() { config.Cfg = saved }) |
| 41 | config.Cfg.Sessions.Enabled = false | 41 | 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, in | 44 | 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 | ||
| 79 | 79 | // The other agent's tool calls are allowed, as in the line mode: the call to |
| 80 | 80 | // `mm --ask` is itself the command a person (or a permission prompt) let run. |
| 81 | 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 | 83 | if !ok { |
| 84 | 84 | return fmt.Errorf("unknown agent %q — agents in this config: %s", req.Agent, agentList()) |
| 85 | 85 | } |
| 86 | + path := ref.Path | |
| 86 | 87 | label := fmt.Sprintf("🗣 asking %s (%s)", req.Agent, Describe(path)) |
| 87 | 88 | if req.WithContext { |
| 88 | 89 | 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 to | 79 | // 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 ( | ||
| 5 | 5 | "strings" |
| 6 | 6 | "sync" |
| 7 | 7 | |
| 8 | + "mm/internal/config" | |
| 8 | 9 | "mm/internal/consent" |
| 9 | 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 | 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, 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 | |
| 134 | 136 | // (allow once / reject once — the asker remembers session answers, not the |
| 135 | 137 | // child), or "cancelled" when it offered none that fits. |
| 136 | 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 | 149 | if c.firstSight(p.ToolCall.ToolCallId) { |
| 148 | 150 | 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 | 154 | want := []sdk.PermissionOptionKind{sdk.PermissionOptionKindRejectOnce, sdk.PermissionOptionKindRejectAlways} |
| 153 | 155 | if allowed { |
| @@ -163,6 +165,22 @@ func (c *client) RequestPermission(ctx context.Context, p sdk.RequestPermissionR | ||
| 163 | 165 | 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 | 184 | // event builds the ui.ToolEvent the asker shows for one of the child's calls. |
| 167 | 185 | // Tool is "<agent>/<tool>" so that "allow for this session" in the window |
| 168 | 186 | // 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, so | 132 | +// 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 the | 133 | +// 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 offered | 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 | ||
| 134 | // (allow once / reject once — the asker remembers session answers, not the | 136 | // (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 first | 150 | 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{}}}, nil | 165 | 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 window | 185 | // 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) { | ||
| 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 | 176 | // The child is started once and reused, with a fresh session per ask. |
| 153 | 177 | func TestAskReusesTheChildWithAFreshSession(t *testing.T) { |
| 154 | 178 | 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) | ||
| 36 | 36 | // } |
| 37 | 37 | // } |
| 38 | 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 | 40 | if !ok { |
| 41 | 41 | return nil, fmt.Errorf("unknown agent %q", req.Agent) |
| 42 | 42 | } |
| 43 | + path := ref.Path | |
| 43 | 44 | transcript := "" |
| 44 | 45 | if req.WithContext { |
| 45 | 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 | 61 | // ask.Label(req) // "🗣 asking bob (ollama/qwen3.5:2b), with the conversation…" |
| 61 | 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 | 64 | if req.WithContext { |
| 64 | 65 | line += ", with the conversation" |
| 65 | 66 | } |
| @@ -71,7 +72,7 @@ func Label(req command.AskRequest) string { | ||
| 71 | 72 | func Missing() []string { |
| 72 | 73 | var warnings []string |
| 73 | 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 | 76 | warnings = append(warnings, fmt.Sprintf("mmConfigs: %s → %s not found — /ask-%s will fail", name, path, name)) |
| 76 | 77 | } |
| 77 | 78 | } |
| @@ -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 []string | 73 | 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) { | ||
| 22 | 22 | if err := os.WriteFile(path, []byte("provider: ollama\nmodel: qwen3.5:2b\n"), 0o644); err != nil { |
| 23 | 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 | 28 | // A plain ask sends the question alone and returns the marked pair. |
| @@ -68,7 +68,7 @@ func TestRunFailures(t *testing.T) { | ||
| 68 | 68 | if len(Missing()) != 0 { |
| 69 | 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 | 72 | if m := Missing(); len(m) != 1 || !strings.Contains(m[0], "riker") { |
| 73 | 73 | t.Errorf("Missing() = %v", m) |
| 74 | 74 | } |
| @@ -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 ( | ||
| 6 | 6 | "regexp" |
| 7 | 7 | "sort" |
| 8 | 8 | "strings" |
| 9 | + | |
| 10 | + "gopkg.in/yaml.v3" | |
| 9 | 11 | ) |
| 10 | 12 | |
| 11 | 13 | // SessionsEnv names the variable that turns session persistence off for one |
| @@ -30,7 +32,7 @@ func (c Config) validateAgents() error { | ||
| 30 | 32 | if strings.HasPrefix(name, "with-context") { |
| 31 | 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 | 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 | 44 | // not depend on who started mm (a terminal, an editor, another mm). |
| 43 | 45 | func resolveAgentPaths(configPath string) { |
| 44 | 46 | 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) { | |
| 47 | 49 | continue |
| 48 | 50 | } |
| 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 | |
| 51 | 54 | } |
| 52 | 55 | } |
| 53 | 56 | } |
| @@ -56,7 +59,7 @@ func resolveAgentPaths(configPath string) { | ||
| 56 | 59 | // editor's menu and of the window's picker. |
| 57 | 60 | // |
| 58 | 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 | 64 | func AgentNames() []string { |
| 62 | 65 | names := make([]string, 0, len(Cfg.MMConfigs)) |
| @@ -66,3 +69,39 @@ func AgentNames() []string { | ||
| 66 | 69 | sort.Strings(names) |
| 67 | 70 | 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 | +} | |
| @@ -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 one | 13 | // 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 | continue | 49 | 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] = abs | 52 | + 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 names | 70 | 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) { | ||
| 24 | 24 | if _, err := Load(path); err != nil { |
| 25 | 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 | 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 | 31 | t.Errorf("bob = %q, want the absolute path unchanged", got) |
| 32 | 32 | } |
| 33 | 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 | 64 | // A name that could not be a command name, or an empty path, is a config |
| 39 | 65 | // error naming the culprit; no mmConfigs at all is fine. |
| 40 | 66 | func TestMMConfigsValidation(t *testing.T) { |
| @@ -47,6 +73,7 @@ func TestMMConfigsValidation(t *testing.T) { | ||
| 47 | 73 | {"mmConfigs:\n ask bob: a.yaml\n", `"ask bob" is not a valid name`}, |
| 48 | 74 | {"mmConfigs:\n -bob: a.yaml\n", `"-bob" is not a valid name`}, |
| 49 | 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 | 77 | {"mmConfigs:\n with-context-bob: a.yaml\n", "it would read as /ask-with-context"}, |
| 51 | 78 | } { |
| 52 | 79 | 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 config | 64 | // 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 = defaults | 79 | Cfg = defaults |
modified
internal/config/config.go +6 -5 | @@ -174,11 +174,12 @@ type Config struct { | ||
| 174 | 174 | // `yolo: false`; any value adds a banner warning (Deprecations). |
| 175 | 175 | Permissions string `yaml:"permissions"` |
| 176 | 176 | |
| 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"` | |
| 182 | 183 | } |
| 183 | 184 | |
| 184 | 185 | // 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>`), name | 177 | + // 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 THIS | 178 | + // 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 name | 179 | + // 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 { | ||
| 241 | 241 | // is hung and we cancel the context. This is the "silent loop" of |
| 242 | 242 | // STALL_DETECTION_PROBLEM.md — a liveness failure, not a behavioural one. |
| 243 | 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 | 245 | // Genkit only hands us back the LAST message. The intermediate turns — "I |
| 245 | 246 | // am calling bash", "here is the output" — exist only in the request it |
| 246 | 247 | // 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 | 410 | defer cancelGen() |
| 410 | 411 | pl = p |
| 411 | 412 | beat := pl.beat |
| 413 | + genCtx = withHeartbeat(genCtx, beat) | |
| 412 | 414 | |
| 413 | 415 | // A thinking model streams its reasoning in a field Genkit drops, so the |
| 414 | 416 | // 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" of | 241 | // 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 — "I | 245 | // 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 it | 246 | // 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 way | 247 | // 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 = p | 411 | pl = p |
| 411 | beat := pl.beat | 412 | beat := pl.beat |
| 413 | + genCtx = withHeartbeat(genCtx, beat) | ||
| 412 | 414 | ||
| 413 | // A thinking model streams its reasoning in a field Genkit drops, so the | 415 | // A thinking model streams its reasoning in a field Genkit drops, so the |
| 414 | // streaming callback below never runs while it thinks (reasoning.go). The | 416 | // 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 | ||
| 15 | 15 | import ( |
| 16 | 16 | "context" |
| 17 | 17 | "testing" |
| 18 | + "time" | |
| 18 | 19 | |
| 19 | 20 | "mm/internal/config" |
| 20 | 21 | "mm/internal/detector" |
| @@ -413,3 +414,64 @@ func TestStuckToolEndsTheTurnAndIsExplained(t *testing.T) { | ||
| 413 | 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 | +} | |
| @@ -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 ( | ||
| 21 | 21 | func (r *runner) startAsk(p Prompt, req command.AskRequest) { |
| 22 | 22 | r.t.AddUser(p.Display()) |
| 23 | 23 | 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) | |
| 27 | 26 | ctx, cancel := context.WithCancel(context.Background()) |
| 28 | 27 | r.cancel = cancel |
| 29 | 28 | r.cancelled.Store(false) |
| @@ -41,8 +40,7 @@ func (r *runner) startAsk(p Prompt, req command.AskRequest) { | ||
| 41 | 40 | // when the other agent answered. |
| 42 | 41 | func (r *runner) applyAsk(res outcome) { |
| 43 | 42 | r.t.Close() |
| 44 | - r.t.SetSpeaker("") | |
| 45 | - r.asking.Store(nil) | |
| 43 | + (&sink{r: r}).Spoken() | |
| 46 | 44 | switch { |
| 47 | 45 | case res.err != nil && res.cancelled: |
| 48 | 46 | 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 = cancel | 27 | 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 | ||
| 46 | 46 | func askRunner(t *testing.T, a *fakeAsker) (*runner, *Transcript) { |
| 47 | 47 | t.Helper() |
| 48 | 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 | 50 | command.SetAgents([]string{"bob"}) |
| 51 | 51 | t.Cleanup(func() { command.SetAgents(nil) }) |
| 52 | 52 | r.opts.Asker = a |
| @@ -110,7 +110,7 @@ func TestWindowAskNamesTheAskedAgent(t *testing.T) { | ||
| 110 | 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 | 111 | t.Fatal(err) |
| 112 | 112 | } |
| 113 | - config.Cfg.MMConfigs["bob"] = path | |
| 113 | + config.Cfg.MMConfigs["bob"] = config.AgentRef{Path: path} | |
| 114 | 114 | var during string |
| 115 | 115 | a.during = func() { during = r.header() } |
| 116 | 116 | |
| @@ -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 = a | 52 | 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"] = path | 113 | + config.Cfg.MMConfigs["bob"] = config.AgentRef{Path: path} |
| 114 | var during string | 114 | 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 | ||
| 3 | 3 | import ( |
| 4 | 4 | "context" |
| 5 | 5 | |
| 6 | + "mm/internal/ask" | |
| 6 | 7 | "mm/internal/config" |
| 7 | 8 | "mm/internal/consent" |
| 8 | 9 | "mm/internal/ui" |
| @@ -15,7 +16,26 @@ import ( | ||
| 15 | 16 | // built-in tools, the MCP wrapper for its own. |
| 16 | 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 | 40 | func (k *sink) Text(text string) { |
| 21 | 41 | 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 { | ||
| 101 | 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 | 116 | // The active sink is set once per turn by the ACP façade and read by the tools |
| 105 | 117 | // from Genkit's goroutines — hence the lock, not a plain variable. |
| 106 | 118 | 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 tools | 116 | // 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() { | ||
| 226 | 226 | // pressed Escape). |
| 227 | 227 | loopDetector := detector.NewLoopDetector(10, 3).WithStop(3) |
| 228 | 228 | |
| 229 | - set := assembleTools(e, loopDetector) | |
| 229 | + set := assembleTools(e, loopDetector, asker) | |
| 230 | 230 | defer set.close() |
| 231 | 231 | agentTools := set.tools |
| 232 | 232 | set.warnings = append(set.warnings, ask.Missing()...) |
| @@ -311,8 +311,10 @@ type toolSet struct { | ||
| 311 | 311 | // (built-in tools), so the two can be compared on the same prompts; |
| 312 | 312 | // `fetch_content` behind `fetchContentTool`, off by default because it is the |
| 313 | 313 | // 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 { | |
| 316 | 318 | set := toolSet{close: func() {}} |
| 317 | 319 | if config.Cfg.BashTool { |
| 318 | 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 | 348 | set.tools = append(set.tools, tools.FetchContent(e.G, d, fetcher)) |
| 347 | 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 | 355 | assembleLSP(e, d, &set) |
| 350 | 356 | assembleMCP(e, d, &set) |
| 351 | 357 | 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.tools | 231 | 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 the | 312 | // `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 on | 313 | // 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 set | 357 | return set |
modified
mini-mes/riker/README.en.md +23 -34 | @@ -1,38 +1,26 @@ | ||
| 1 | 1 | # 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 | 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`) | | |
| 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 | | |
| 10 | 10 | |
| 11 | 11 | ## 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.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 | |
| 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. | |
| 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. | |
| 27 | 20 | |
| 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)]`. | |
| 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 | 25 | ## Run |
| 38 | 26 | |
| @@ -45,25 +33,26 @@ mm -config /path/to/mini-me/mini-mes/riker/agent.riker.yaml | ||
| 45 | 33 | |
| 46 | 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 | 38 | ## 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. | |
| 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`. | |
| 56 | 44 | |
| 57 | 45 | ## 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. | |
| 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). | |
| 61 | 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. | |
| 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. | |
| 64 | 53 | |
| 65 | 54 | ## See also |
| 66 | 55 | |
| 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 specialists | 1 | # 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 flow | 11 | ## 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.md | 14 | +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 | ## Run | 25 | ## 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 this | 38 | ## 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 settings | 45 | ## 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 also | 54 | ## 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 @@ | ||
| 1 | 1 | # 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 | 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`) | | |
| 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 | | |
| 10 | 10 | |
| 11 | 11 | ## 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.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 | |
| 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. | |
| 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. | |
| 27 | 20 | |
| 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)]`. | |
| 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 | 25 | ## Lancer |
| 38 | 26 | |
| @@ -45,25 +33,26 @@ mm -config /chemin/vers/mini-me/mini-mes/riker/agent.riker.yaml | ||
| 45 | 33 | |
| 46 | 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 | 38 | ## 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. | |
| 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. | |
| 56 | 44 | |
| 57 | 45 | ## 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. | |
| 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). | |
| 61 | 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. | |
| 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é. | |
| 64 | 53 | |
| 65 | 54 | ## Voir aussi |
| 66 | 55 | |
| 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écialistes | 1 | # 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 circuit | 11 | ## 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.md | 14 | +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 | ## Lancer | 25 | ## 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 — à lire | 38 | ## 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 notables | 45 | ## 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 aussi | 54 | ## 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: | |
| 3 | 3 | # `mm -config agent.homer.yaml`, from the project. |
| 4 | 4 | # |
| 5 | 5 | # Started by riker as a child process (`mm -acp -config agent.homer.yaml`) |
| @@ -14,6 +14,7 @@ provider: llmman | ||
| 14 | 14 | # long edit/test loops. ~48 GB at Q4_K_M. |
| 15 | 15 | #model: hf.co/unsloth/Qwen3-Coder-Next-GGUF:Q4_K_M |
| 16 | 16 | 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 | 19 | baseUrl: http://127.0.0.1:17434/v1 |
| 19 | 20 | fallback: http://host.docker.internal:17434/v1 |
| @@ -71,9 +72,9 @@ displayCommands: true | ||
| 71 | 72 | |
| 72 | 73 | # ─── Tools ─────────────────────────────────────────────────────────────────── |
| 73 | 74 | |
| 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: | |
| 77 | 78 | # every change waits for you. |
| 78 | 79 | yolo: false |
| 79 | 80 | |
| @@ -88,18 +89,26 @@ planTool: false | ||
| 88 | 89 | # Reading a library's documentation on the web. |
| 89 | 90 | fetchContentTool: true |
| 90 | 91 | |
| 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 | |
| 103 | 112 | # file when riker asks (ACP mode) but the current directory when you run |
| 104 | 113 | # homer directly in a terminal. Adjust it if you move the repository. |
| 105 | 114 | skillsDir: /Users/k33g/kDrive/Rickub/bots-garden/mini-me/mini-mes/riker/skills-homer |
| @@ -119,7 +128,7 @@ context: | ||
| 119 | 128 | |
| 120 | 129 | acp: |
| 121 | 130 | name: homer |
| 122 | - title: Homer (coder) | |
| 131 | + title: 🐻❄️ Homer (coder) | |
| 123 | 132 | |
| 124 | 133 | system: | |
| 125 | 134 | Your name is Homer. You are a senior software engineer working in a |
| @@ -134,6 +143,10 @@ system: | | ||
| 134 | 143 | - `read_file`, `edit_file`, `write_file` read and change files. Read before |
| 135 | 144 | you write. Change existing files with `edit_file` (`old` copied exactly, |
| 136 | 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 | 150 | - `fetch_content` reads documentation pages. |
| 138 | 151 | |
| 139 | 152 | RULES |
| @@ -1,5 +1,5 @@ | |||
| 1 | -# Homer — the coder. Asked by riker (`AGENT_YOLO=true mm --ask homer`) to | 1 | +# 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_M | 15 | #model: hf.co/unsloth/Qwen3-Coder-Next-GGUF:Q4_K_M |
| 16 | model: hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M | 16 | 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/v1 | 19 | baseUrl: http://127.0.0.1:17434/v1 |
| 19 | fallback: http://host.docker.internal:17434/v1 | 20 | 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 skill | 75 | +# 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 interruption | 76 | +# 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: false | 79 | 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: true | 90 | 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: gopls | 95 | +# go install golang.org/x/tools/gopls@latest |
| 95 | -# command: gopls | 96 | +# 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: typescript | 98 | +# gopls starts on homer's first lsp call, in the project directory. |
| 98 | -# command: typescript-language-server | 99 | +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 this | 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 | ||
| 103 | # file when riker asks (ACP mode) but the current directory when you run | 112 | # 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-homer | 114 | 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: homer | 130 | 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 a | 134 | 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 before | 143 | - `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 | RULES | 152 | 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`. | |
| 3 | 3 | # |
| 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. | |
| 7 | 7 | # |
| 8 | 8 | # Started by riker as a child process (`mm -acp -config agent.milo.yaml`) in |
| 9 | 9 | # riker's working directory — the project. |
| @@ -68,17 +68,16 @@ displayCommands: true | ||
| 68 | 68 | |
| 69 | 69 | # ─── Tools ─────────────────────────────────────────────────────────────────── |
| 70 | 70 | |
| 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. | |
| 75 | 74 | 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 | 77 | bashTool: false |
| 79 | 78 | |
| 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/. | |
| 82 | 81 | editTools: true |
| 83 | 82 | |
| 84 | 83 | planTool: false |
| @@ -103,22 +102,23 @@ context: | ||
| 103 | 102 | |
| 104 | 103 | acp: |
| 105 | 104 | name: milo |
| 106 | - title: Milo (specifier) | |
| 105 | + title: 🐼 Milo (specifier) | |
| 107 | 106 | |
| 108 | 107 | system: | |
| 109 | 108 | 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 | 111 | 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`. | |
| 116 | 115 | |
| 117 | 116 | 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: | |
| 122 | 122 | |
| 123 | 123 | ## Goal |
| 124 | 124 | ## Context |
| @@ -138,14 +138,18 @@ system: | | ||
| 138 | 138 | - Edge cases and errors: inputs and failures, and the expected behaviour. |
| 139 | 139 | - Acceptance criteria: numbered A1, A2…, Given / When / Then, each |
| 140 | 140 | traceable to a requirement, with exact commands and outputs when the |
| 141 | - brief gives examples. | |
| 141 | + request gives examples. | |
| 142 | 142 | - 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 | |
| 144 | 144 | the default you chose so the coder is never blocked. |
| 145 | 145 | |
| 146 | 146 | Write for a coder who will read nothing else: concrete, no filler, no |
| 147 | 147 | 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 | 154 | JOB 2 — CHECK AN IMPLEMENTATION |
| 151 | 155 | When asked to check code against a specification: read the spec, then the |
| @@ -155,5 +159,5 @@ system: | | ||
| 155 | 159 | from what you read. |
| 156 | 160 | |
| 157 | 161 | RULES |
| 158 | - - No greeting, no preamble, no closing remark: the document only. | |
| 162 | + - No greeting, no preamble, no closing remark. | |
| 159 | 163 | - Answer in English. |
| @@ -1,9 +1,9 @@ | |||
| 1 | -# Milo — the specifier. Asked by riker (`mm --ask milo`), never used directly | 1 | +# 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 implementation | 4 | +# 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's | 5 | +# 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`) in | 8 | # 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 always | 71 | +# 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 and | 72 | +# 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 his | 73 | +# This value applies only when you run milo directly. |
| 74 | -# prompt says so too. | ||
| 75 | yolo: false | 74 | 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: false | 77 | bashTool: false |
| 79 | 78 | ||
| 80 | -# read_file — to read the brief and the code it mentions. (write_file and | 79 | +# 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: true | 81 | editTools: true |
| 83 | 82 | ||
| 84 | planTool: false | 83 | planTool: false |
| @@ -103,22 +102,23 @@ context: | |||
| 103 | 102 | ||
| 104 | acp: | 103 | acp: |
| 105 | name: milo | 104 | 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 a | 108 | 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 TOOLS | 111 | YOUR TOOLS |
| 113 | - `read_file` only. Read the files you are told to read, and the files they | 112 | + `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. Your | 113 | + 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 SPECIFICATION | 116 | JOB 1 — WRITE A SPECIFICATION |
| 118 | - When asked to specify from a brief: read the brief, read the existing code | 117 | + 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 a | 118 | + write the specification with `write_file`, in Markdown, to the path you |
| 120 | - line `# <Title>`, then these eight `##` headings, written exactly as | 119 | + 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 | ## Goal | 123 | ## Goal |
| 124 | ## Context | 124 | ## 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, each | 139 | - Acceptance criteria: numbered A1, A2…, Given / When / Then, each |
| 140 | traceable to a requirement, with exact commands and outputs when the | 140 | 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 with | 143 | + - 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, no | 146 | Write for a coder who will read nothing else: concrete, no filler, no |
| 147 | code beyond signatures and short examples. Language and stack: those of | 147 | 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 IMPLEMENTATION | 154 | JOB 2 — CHECK AN IMPLEMENTATION |
| 151 | When asked to check code against a specification: read the spec, then the | 155 | 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 | RULES | 161 | 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 @@ | ||
| 1 | 1 | # 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 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. | |
| 6 | 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: | |
| 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. | |
| 14 | 12 | # |
| 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: | |
| 17 | 19 | # |
| 18 | 20 | # cd ~/src/my-project |
| 19 | 21 | # mm -config /path/to/mini-mes/riker/agent.riker.yaml |
| @@ -28,7 +30,8 @@ | ||
| 28 | 30 | provider: llmman |
| 29 | 31 | |
| 30 | 32 | # 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 | 35 | model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M |
| 33 | 36 | |
| 34 | 37 | # `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 | 39 | baseUrl: http://127.0.0.1:17434/v1 |
| 37 | 40 | fallback: http://host.docker.internal:17434/v1 |
| 38 | 41 | |
| 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). | |
| 42 | 52 | 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 | |
| 45 | 71 | |
| 46 | 72 | # Tokens served. 0 = ask llmman once the model is loaded (131,072 for this |
| 47 | 73 | # model, its trained maximum, unless LLMMAN_CONTEXT_LENGTH says less). |
| @@ -51,7 +77,7 @@ contextWindow: 0 | ||
| 51 | 77 | |
| 52 | 78 | sampling: |
| 53 | 79 | # 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 | |
| 55 | 81 | # temperature is lowered; top_p / top_k stay as Gemma recommends. |
| 56 | 82 | temperature: 0.3 |
| 57 | 83 | top_p: 0.95 |
| @@ -60,69 +86,58 @@ sampling: | ||
| 60 | 86 | # Riker's replies are short: a tool call, or a summary. |
| 61 | 87 | 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 | 90 | parallel_tool_calls: false |
| 65 | 91 | |
| 66 | 92 | # 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. | |
| 69 | 95 | chat_template_kwargs: |
| 70 | - enable_thinking: true | |
| 96 | + enable_thinking: false | |
| 71 | 97 | |
| 72 | -# Display of the reasoning, if you turn it on above. | |
| 73 | 98 | showThinking: true |
| 74 | 99 | |
| 75 | 100 | # ─── Timeouts ──────────────────────────────────────────────────────────────── |
| 76 | 101 | |
| 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 | |
| 86 | 107 | |
| 87 | 108 | # Silence tolerated while the server reads the prompt (and loads the model on |
| 88 | 109 | # the first question). Progress reports restart it. |
| 89 | 110 | firstTokenTimeout: 3m |
| 90 | 111 | |
| 112 | +# Riker runs only short commands itself (ls, cat). | |
| 113 | +bashTimeout: 1m | |
| 114 | + | |
| 91 | 115 | # ─── Agent loop ────────────────────────────────────────────────────────────── |
| 92 | 116 | |
| 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. | |
| 95 | 119 | maxTurns: 20 |
| 96 | 120 | |
| 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. | |
| 99 | 123 | 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 | 125 | previewLines: 20 |
| 104 | 126 | displayCommands: true |
| 105 | 127 | |
| 106 | 128 | # ─── Tools ─────────────────────────────────────────────────────────────────── |
| 107 | 129 | |
| 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. | |
| 115 | 133 | # With `mm -p` nobody can answer: run with AGENT_YOLO=true, knowingly. |
| 116 | 134 | yolo: false |
| 117 | 135 | |
| 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. | |
| 122 | 137 | bashTool: true |
| 123 | 138 | |
| 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. | |
| 126 | 141 | editTools: true |
| 127 | 142 | |
| 128 | 143 | # No plan tool, no web, no language server: fewer tools = fewer wrong picks |
| @@ -130,12 +145,9 @@ editTools: true | ||
| 130 | 145 | planTool: false |
| 131 | 146 | fetchContentTool: false |
| 132 | 147 | |
| 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 | |
| 139 | 151 | |
| 140 | 152 | # ─── Memory ────────────────────────────────────────────────────────────────── |
| 141 | 153 | |
| @@ -160,30 +172,42 @@ context: | ||
| 160 | 172 | |
| 161 | 173 | acp: |
| 162 | 174 | name: riker |
| 163 | - title: Riker (coordinator) | |
| 175 | + title: 🐻 Riker (coordinator) | |
| 164 | 176 | |
| 165 | 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 | 180 | - milo writes specifications and checks code against them; |
| 168 | 181 | - 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). | |
| 179 | 203 | |
| 180 | 204 | 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. | |
| 187 | 211 | - Answer in the language the user wrote in — French if they wrote French — |
| 188 | 212 | even though milo and homer answer you in English. A few lines: who did |
| 189 | 213 | what, which files, what is next. |
| @@ -1,19 +1,21 @@ | |||
| 1 | # Riker — the coordinator. A small, fast model that talks with you and hands | 1 | # 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 request | 4 | +# 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 same | 13 | +# 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-project | 20 | # cd ~/src/my-project |
| 19 | # mm -config /path/to/mini-mes/riker/agent.riker.yaml | 21 | # mm -config /path/to/mini-mes/riker/agent.riker.yaml |
| @@ -28,7 +30,8 @@ | |||
| 28 | provider: llmman | 30 | provider: llmman |
| 29 | 31 | ||
| 30 | # Gemma 4 E4B (7.5B parameters, ~4B active per token): quick answers, good | 32 | # 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_M | 35 | 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 Docker | 37 | # `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/v1 | 39 | baseUrl: http://127.0.0.1:17434/v1 |
| 37 | fallback: http://host.docker.internal:17434/v1 | 40 | fallback: http://host.docker.internal:17434/v1 |
| 38 | 41 | ||
| 39 | -# The agents riker can ask. Names become /ask-milo, /ask-homer (and | 42 | +# The agents riker can hand work to. Each one is: |
| 40 | -# /ask-with-context-…) for YOU to type, and `"$MM_BIN" --ask <name>` for the | 43 | +# - 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.yaml | 53 | + milo: |
| 44 | - homer: agent.homer.yaml | 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 | ||
| 45 | 71 | ||
| 46 | # Tokens served. 0 = ask llmman once the model is loaded (131,072 for this | 72 | # 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 for | 79 | # 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 |
| 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.3 | 82 | temperature: 0.3 |
| 57 | top_p: 0.95 | 83 | 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: 4096 | 87 | 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: false | 90 | parallel_tool_calls: false |
| 65 | 91 | ||
| 66 | # Gemma 4's template reads `enable_thinking` and defaults it to false | 92 | # 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 | 93 | + # (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: true | 96 | + enable_thinking: false |
| 71 | 97 | ||
| 72 | -# Display of the reasoning, if you turn it on above. | ||
| 73 | showThinking: true | 98 | showThinking: true |
| 74 | 99 | ||
| 75 | # ─── Timeouts ──────────────────────────────────────────────────────────────── | 100 | # ─── Timeouts ──────────────────────────────────────────────────────────────── |
| 76 | 101 | ||
| 77 | -# One bash command — that is, one whole `--ask` to milo or homer — may run | 102 | +# 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 many | 103 | +# 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: 30m | 105 | +# 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 on | 108 | # 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: 3m | 110 | 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: 20 | 119 | maxTurns: 20 |
| 96 | 120 | ||
| 97 | -# Characters of a tool result handed back to riker. A spec printed after milo | 121 | +# 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: 16000 | 123 | 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: 20 | 125 | previewLines: 20 |
| 104 | displayCommands: true | 126 | 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, and | 132 | +# [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: false | 134 | 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: true | 137 | bashTool: true |
| 123 | 138 | ||
| 124 | -# read_file / write_file / edit_file: riker writes the BRIEF with write_file | 139 | +# 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: true | 141 | editTools: true |
| 127 | 142 | ||
| 128 | # No plan tool, no web, no language server: fewer tools = fewer wrong picks | 143 | # No plan tool, no web, no language server: fewer tools = fewer wrong picks |
| @@ -130,12 +145,9 @@ editTools: true | |||
| 130 | planTool: false | 145 | planTool: false |
| 131 | fetchContentTool: false | 146 | 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 starts | 150 | +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: riker | 174 | 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 WORK | 184 | + HOW TO BUILD SOMETHING |
| 172 | - You have skills, listed in the `read_skill` tool. For any request to | 185 | + When the user asks to build, create, add, change or fix something: |
| 173 | - specify, design, build, implement, code, fix or check something, call | 186 | + 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 it | 190 | + 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 | RULES | 204 | RULES |
| 181 | - - Run each hand-off command ONCE. What it prints is the answer: never run | 205 | + - 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 did | 212 | 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 | |
| 2 | 2 | |
| 3 | 3 | go 1.27 |
| @@ -1,3 +1,3 @@ | |||
| 1 | -module notes | 1 | +module hello-world-json |
| 2 | 2 | ||
| 3 | go 1.27 | 3 | 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 | ||
| 14 | 14 | |
| 15 | 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 | 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 | 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`…). |
| @@ -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`…). |
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`) | ||