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

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

Download ZIP

Merge feature/quality-pass into main

rickub committed 2026-10-16T09:10:07+02:00 Browse files
5d7d40f parents: b0741ef d86eca0
modified 02-build-releases.sh +1 -0
@@ -135,6 +135,7 @@ downloadTable() {
135135 printf '| Platform | Download |\n|---|---|\n'
136136 for platform in "${PLATFORMS[@]}"; do
137137 local goos="${platform%/*}" goarch="${platform#*/}"
138+ # shellcheck disable=SC2016 # the backticks are Markdown code spans, not a command substitution
138139 printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"
139140 done
140141 }
@@ -135,6 +135,7 @@ downloadTable() {
135 printf '| Platform | Download |\n|---|---|\n'135 printf '| Platform | Download |\n|---|---|\n'
136 for platform in "${PLATFORMS[@]}"; do136 for platform in "${PLATFORMS[@]}"; do
137 local goos="${platform%/*}" goarch="${platform#*/}"137 local goos="${platform%/*}" goarch="${platform#*/}"
138+ # shellcheck disable=SC2016 # the backticks are Markdown code spans, not a command substitution
138 printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"139 printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"
139 done140 done
140 }141 }
modified README.md +1 -0
@@ -129,6 +129,7 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h
129129 | `demos/llmman/` | mm on llmman, and an agent that asks other agents |
130130 | `demos/ollama/` | mm on Ollama |
131131 | `demos/structured-output/` | agents that answer in JSON, and a coordinator using them |
132+| `demos/structured-output-pizza/` | one JSON agent declared with Go types: a pizza's name → its ingredients (quantity, unit, category, allergens), whether it is vegetarian, its baking time (Gemma 4 E4B on llmman) |
132133 | `demos/agents-md-file/` | an agent following a project's `AGENTS.md` |
133134 | `demos/mini-mes/` | small teams of agents (`riker` coordinating a specifier and a coder, `gopher` a Go specialist) |
134135 | `demos/little-coder/` | a Go + htmx web-app generator for a small local model (Mellum2 12B on llmman, 32 GB RAM): a tested starter, skills, `AGENTS.md`, measured results |
@@ -129,6 +129,7 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h
129 | `demos/llmman/` | mm on llmman, and an agent that asks other agents |129 | `demos/llmman/` | mm on llmman, and an agent that asks other agents |
130 | `demos/ollama/` | mm on Ollama |130 | `demos/ollama/` | mm on Ollama |
131 | `demos/structured-output/` | agents that answer in JSON, and a coordinator using them |131 | `demos/structured-output/` | agents that answer in JSON, and a coordinator using them |
132+| `demos/structured-output-pizza/` | one JSON agent declared with Go types: a pizza's name → its ingredients (quantity, unit, category, allergens), whether it is vegetarian, its baking time (Gemma 4 E4B on llmman) |
132 | `demos/agents-md-file/` | an agent following a project's `AGENTS.md` |133 | `demos/agents-md-file/` | an agent following a project's `AGENTS.md` |
133 | `demos/mini-mes/` | small teams of agents (`riker` coordinating a specifier and a coder, `gopher` a Go specialist) |134 | `demos/mini-mes/` | small teams of agents (`riker` coordinating a specifier and a coder, `gopher` a Go specialist) |
134 | `demos/little-coder/` | a Go + htmx web-app generator for a small local model (Mellum2 12B on llmman, 32 GB RAM): a tested starter, skills, `AGENTS.md`, measured results |135 | `demos/little-coder/` | a Go + htmx web-app generator for a small local model (Mellum2 12B on llmman, 32 GB RAM): a tested starter, skills, `AGENTS.md`, measured results |
modified demos/dungeon/guard.sh +1 -0
@@ -39,6 +39,7 @@ case "$kind" in
3939 command=$rest
4040 rewrite=1
4141 fi
42+ # shellcheck disable=SC2016 # '$(' is the literal text being refused, not an expansion
4243 case "$command" in
4344 *';'*|*'&'*|*'|'*|*'`'*|*'$('*|*'>'*|*'<'*|*'\n'*)
4445 deny "one command at a time, nothing chained or redirected. Your commands: $hint" ;;
@@ -39,6 +39,7 @@ case "$kind" in
39 command=$rest39 command=$rest
40 rewrite=140 rewrite=1
41 fi41 fi
42+ # shellcheck disable=SC2016 # '$(' is the literal text being refused, not an expansion
42 case "$command" in43 case "$command" in
43 *';'*|*'&'*|*'|'*|*'`'*|*'$('*|*'>'*|*'<'*|*'\n'*)44 *';'*|*'&'*|*'|'*|*'`'*|*'$('*|*'>'*|*'<'*|*'\n'*)
44 deny "one command at a time, nothing chained or redirected. Your commands: $hint" ;;45 deny "one command at a time, nothing chained or redirected. Your commands: $hint" ;;
modified demos/dungeon/sheet.sh +3 -1
@@ -135,7 +135,9 @@ meet() {
135135 }
136136
137137 monster() {
138- [ $# -eq 2 ] && is_signed "$2" || die 'usage: sh sheet.sh monster "<Name>" -5'
138+ if [ $# -ne 2 ] || ! is_signed "$2"; then
139+ die 'usage: sh sheet.sh monster "<Name>" -5'
140+ fi
139141 not_after_attack
140142 line=$(sed -n '/^## Encounter/,$p' "$SHEET" | grep -F -- "- $1:" | head -1 || true)
141143 [ -n "$line" ] || die "no monster \"$1\" in ## Encounter"
@@ -135,7 +135,9 @@ meet() {
135 }135 }
136 136
137 monster() {137 monster() {
138- [ $# -eq 2 ] && is_signed "$2" || die 'usage: sh sheet.sh monster "<Name>" -5'138+ if [ $# -ne 2 ] || ! is_signed "$2"; then
139+ die 'usage: sh sheet.sh monster "<Name>" -5'
140+ fi
139 not_after_attack141 not_after_attack
140 line=$(sed -n '/^## Encounter/,$p' "$SHEET" | grep -F -- "- $1:" | head -1 || true)142 line=$(sed -n '/^## Encounter/,$p' "$SHEET" | grep -F -- "- $1:" | head -1 || true)
141 [ -n "$line" ] || die "no monster \"$1\" in ## Encounter"143 [ -n "$line" ] || die "no monster \"$1\" in ## Encounter"
added demos/structured-output-pizza/README.en.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# JSON answers from Go types — a pizza's ingredients
2+
3+*[Version française](README.fr.md)*
4+
5+One agent that answers **only in JSON**: name a pizza, get its ingredients for one 30 cm pizza. The shape of the answer is declared as Go types under `structuredOutput` in `agent.yaml`.
6+
7+Model: **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`) on **llmman**.
8+
9+## Run
10+
11+```bash
12+llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
13+cd demos/structured-output-pizza
14+mm -p "A four-cheese pizza" | jq .
15+```
16+
17+In prompt mode (`-p`), stdout holds the JSON alone, so it pipes into `jq`.
18+
19+## The declaration
20+
21+```go
22+// One ingredient of the pizza.
23+type Ingredient struct {
24+ Name string `json:"name"` // the ingredient's name, in the user's language
25+ Quantity float64 `json:"quantity"` // amount for one pizza
26+ Unit string `json:"unit"` // "g", "ml", "piece", "pinch" or "to taste"
27+ Category string `json:"category"` // "base", "sauce", "cheese", "meat", "vegetable", "herb" or "other"
28+ Allergens []string `json:"allergens"` // e.g. "gluten", "lactose"; empty when none
29+ Optional bool `json:"optional"`
30+}
31+
32+type Pizza struct {
33+ Name string `json:"name"`
34+ Vegetarian bool `json:"vegetarian"`
35+ Ingredients []Ingredient `json:"ingredients"`
36+ BakingTime *int `json:"bakingTime"` // minutes, null when unknown
37+}
38+
39+var Recipe Pizza
40+```
41+
42+- The `var` names the root of the answer: here one `Pizza` object. `var Recipes = []Pizza{}` would ask for an array.
43+- The `//` comments become the fields' `description` in the JSON Schema; they are where the allowed values go, since `const` (and so enums) is refused.
44+- Every key is required; a pointer (`*int`) also allows `null`.
45+- An agent with `structuredOutput` declares no tool: `bashTool`, `editTools`, `planTool`, `fetchContentTool`, `askUserTool` are `false` and `skillsDir` is `no-skills`.
46+
47+## Seen on 2026-10-16
48+
49+`mm -p "A four-cheese pizza"` (about 40 s):
50+
51+```
52+Four-Cheese Pizza — vegetarian: true — bakingTime: 12
53+Pizza Dough 300 g (base), Tomato Sauce 50 g (sauce), Mozzarella 100 g, Gorgonzola 50 g,
54+Provolone 50 g, Parmesan 20 g (cheese), Olive Oil 15 ml (other)
55+```
56+
57+`mm -p "Une pizza margherita"`: five ingredients, names in Italian (`Mozzarella di bufala`, `Basilico fresco`…) — the model chose the dish's own language over the question's.
58+
59+## Further reading
60+
61+- Every Go construct and its JSON Schema: [configuration reference — `structuredOutput`](../../docs/en/reference/configuration.md#structuredoutput)
62+- Step by step: [Get answers as JSON](../../docs/en/how-to/get-json-answers.md)
63+- Several JSON agents and a coordinator: `demos/structured-output/`
new file mode 100644
@@ -0,0 +1,63 @@
1+# JSON answers from Go types — a pizza's ingredients
2+
3+*[Version française](README.fr.md)*
4+
5+One agent that answers **only in JSON**: name a pizza, get its ingredients for one 30 cm pizza. The shape of the answer is declared as Go types under `structuredOutput` in `agent.yaml`.
6+
7+Model: **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`) on **llmman**.
8+
9+## Run
10+
11+```bash
12+llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
13+cd demos/structured-output-pizza
14+mm -p "A four-cheese pizza" | jq .
15+```
16+
17+In prompt mode (`-p`), stdout holds the JSON alone, so it pipes into `jq`.
18+
19+## The declaration
20+
21+```go
22+// One ingredient of the pizza.
23+type Ingredient struct {
24+ Name string `json:"name"` // the ingredient's name, in the user's language
25+ Quantity float64 `json:"quantity"` // amount for one pizza
26+ Unit string `json:"unit"` // "g", "ml", "piece", "pinch" or "to taste"
27+ Category string `json:"category"` // "base", "sauce", "cheese", "meat", "vegetable", "herb" or "other"
28+ Allergens []string `json:"allergens"` // e.g. "gluten", "lactose"; empty when none
29+ Optional bool `json:"optional"`
30+}
31+
32+type Pizza struct {
33+ Name string `json:"name"`
34+ Vegetarian bool `json:"vegetarian"`
35+ Ingredients []Ingredient `json:"ingredients"`
36+ BakingTime *int `json:"bakingTime"` // minutes, null when unknown
37+}
38+
39+var Recipe Pizza
40+```
41+
42+- The `var` names the root of the answer: here one `Pizza` object. `var Recipes = []Pizza{}` would ask for an array.
43+- The `//` comments become the fields' `description` in the JSON Schema; they are where the allowed values go, since `const` (and so enums) is refused.
44+- Every key is required; a pointer (`*int`) also allows `null`.
45+- An agent with `structuredOutput` declares no tool: `bashTool`, `editTools`, `planTool`, `fetchContentTool`, `askUserTool` are `false` and `skillsDir` is `no-skills`.
46+
47+## Seen on 2026-10-16
48+
49+`mm -p "A four-cheese pizza"` (about 40 s):
50+
51+```
52+Four-Cheese Pizza — vegetarian: true — bakingTime: 12
53+Pizza Dough 300 g (base), Tomato Sauce 50 g (sauce), Mozzarella 100 g, Gorgonzola 50 g,
54+Provolone 50 g, Parmesan 20 g (cheese), Olive Oil 15 ml (other)
55+```
56+
57+`mm -p "Une pizza margherita"`: five ingredients, names in Italian (`Mozzarella di bufala`, `Basilico fresco`…) — the model chose the dish's own language over the question's.
58+
59+## Further reading
60+
61+- Every Go construct and its JSON Schema: [configuration reference — `structuredOutput`](../../docs/en/reference/configuration.md#structuredoutput)
62+- Step by step: [Get answers as JSON](../../docs/en/how-to/get-json-answers.md)
63+- Several JSON agents and a coordinator: `demos/structured-output/`
added demos/structured-output-pizza/README.fr.md +57 -0
new file mode 100644
@@ -0,0 +1,57 @@
1+# Des réponses JSON à partir de types Go — les ingrédients d'une pizza
2+
3+*[English version](README.en.md)*
4+
5+Un agent qui répond **uniquement en JSON** : nommez une pizza, recevez ses ingrédients pour une pizza de 30 cm. La forme de la réponse est déclarée en types Go sous `structuredOutput` dans `agent.yaml`.
6+
7+Modèle : **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`) sur **llmman**.
8+
9+## Lancer
10+
11+```bash
12+llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
13+cd demos/structured-output-pizza
14+mm -p "Une pizza quatre fromages" | jq .
15+```
16+
17+En mode prompt (`-p`), stdout ne contient que le JSON : on peut le passer à `jq`.
18+
19+## La déclaration
20+
21+```go
22+// One ingredient of the pizza.
23+type Ingredient struct {
24+ Name string `json:"name"` // the ingredient's name, in the user's language
25+ Quantity float64 `json:"quantity"` // amount for one pizza
26+ Unit string `json:"unit"` // "g", "ml", "piece", "pinch" or "to taste"
27+ Category string `json:"category"` // "base", "sauce", "cheese", "meat", "vegetable", "herb" or "other"
28+ Allergens []string `json:"allergens"` // e.g. "gluten", "lactose"; empty when none
29+ Optional bool `json:"optional"`
30+}
31+
32+type Pizza struct {
33+ Name string `json:"name"`
34+ Vegetarian bool `json:"vegetarian"`
35+ Ingredients []Ingredient `json:"ingredients"`
36+ BakingTime *int `json:"bakingTime"` // minutes, null when unknown
37+}
38+
39+var Recipe Pizza
40+```
41+
42+- Le `var` désigne la racine de la réponse : ici un objet `Pizza`. `var Recipes = []Pizza{}` demanderait un tableau.
43+- Les commentaires `//` deviennent la `description` des champs dans le JSON Schema ; c'est là qu'on liste les valeurs permises, puisque `const` (et donc les énumérations) est refusé.
44+- Toutes les clés sont obligatoires ; un pointeur (`*int`) autorise en plus `null`.
45+- Un agent avec `structuredOutput` ne déclare aucun outil : `bashTool`, `editTools`, `planTool`, `fetchContentTool`, `askUserTool` valent `false` et `skillsDir` vaut `no-skills`.
46+
47+## Observé le 2026-10-16
48+
49+`mm -p "Une pizza quatre fromages"` (environ 40 s) : `"name": "Quatre Fromages"`, `"vegetarian": true`, une pâte, une sauce, quatre fromages (dont `{"name":"Brie","quantity":50.0,"unit":"g","category":"cheese","allergens":["lactose"],"optional":false}`) et du thym facultatif (`"unit":"pinch"`, `"optional":true`).
50+
51+`mm -p "Une pizza margherita"` : cinq ingrédients et `"bakingTime": 12`, avec des noms en italien (`Mozzarella di bufala`, `Basilico fresco`…) — le modèle a préféré la langue du plat à celle de la question.
52+
53+## Pour aller plus loin
54+
55+- Chaque construction Go et son JSON Schema : [référence de configuration — `structuredOutput`](../../docs/fr/reference/configuration.md#structuredoutput)
56+- Pas à pas : [Obtenir des réponses en JSON](../../docs/fr/how-to/get-json-answers.md)
57+- Plusieurs agents JSON et un coordinateur : `demos/structured-output/`
new file mode 100644
@@ -0,0 +1,57 @@
1+# Des réponses JSON à partir de types Go — les ingrédients d'une pizza
2+
3+*[English version](README.en.md)*
4+
5+Un agent qui répond **uniquement en JSON** : nommez une pizza, recevez ses ingrédients pour une pizza de 30 cm. La forme de la réponse est déclarée en types Go sous `structuredOutput` dans `agent.yaml`.
6+
7+Modèle : **Gemma 4 E4B** (`hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M`) sur **llmman**.
8+
9+## Lancer
10+
11+```bash
12+llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
13+cd demos/structured-output-pizza
14+mm -p "Une pizza quatre fromages" | jq .
15+```
16+
17+En mode prompt (`-p`), stdout ne contient que le JSON : on peut le passer à `jq`.
18+
19+## La déclaration
20+
21+```go
22+// One ingredient of the pizza.
23+type Ingredient struct {
24+ Name string `json:"name"` // the ingredient's name, in the user's language
25+ Quantity float64 `json:"quantity"` // amount for one pizza
26+ Unit string `json:"unit"` // "g", "ml", "piece", "pinch" or "to taste"
27+ Category string `json:"category"` // "base", "sauce", "cheese", "meat", "vegetable", "herb" or "other"
28+ Allergens []string `json:"allergens"` // e.g. "gluten", "lactose"; empty when none
29+ Optional bool `json:"optional"`
30+}
31+
32+type Pizza struct {
33+ Name string `json:"name"`
34+ Vegetarian bool `json:"vegetarian"`
35+ Ingredients []Ingredient `json:"ingredients"`
36+ BakingTime *int `json:"bakingTime"` // minutes, null when unknown
37+}
38+
39+var Recipe Pizza
40+```
41+
42+- Le `var` désigne la racine de la réponse : ici un objet `Pizza`. `var Recipes = []Pizza{}` demanderait un tableau.
43+- Les commentaires `//` deviennent la `description` des champs dans le JSON Schema ; c'est là qu'on liste les valeurs permises, puisque `const` (et donc les énumérations) est refusé.
44+- Toutes les clés sont obligatoires ; un pointeur (`*int`) autorise en plus `null`.
45+- Un agent avec `structuredOutput` ne déclare aucun outil : `bashTool`, `editTools`, `planTool`, `fetchContentTool`, `askUserTool` valent `false` et `skillsDir` vaut `no-skills`.
46+
47+## Observé le 2026-10-16
48+
49+`mm -p "Une pizza quatre fromages"` (environ 40 s) : `"name": "Quatre Fromages"`, `"vegetarian": true`, une pâte, une sauce, quatre fromages (dont `{"name":"Brie","quantity":50.0,"unit":"g","category":"cheese","allergens":["lactose"],"optional":false}`) et du thym facultatif (`"unit":"pinch"`, `"optional":true`).
50+
51+`mm -p "Une pizza margherita"` : cinq ingrédients et `"bakingTime": 12`, avec des noms en italien (`Mozzarella di bufala`, `Basilico fresco`…) — le modèle a préféré la langue du plat à celle de la question.
52+
53+## Pour aller plus loin
54+
55+- Chaque construction Go et son JSON Schema : [référence de configuration — `structuredOutput`](../../docs/fr/reference/configuration.md#structuredoutput)
56+- Pas à pas : [Obtenir des réponses en JSON](../../docs/fr/how-to/get-json-answers.md)
57+- Plusieurs agents JSON et un coordinateur : `demos/structured-output/`
added demos/structured-output-pizza/agent.yaml +43 -0
new file mode 100644
@@ -0,0 +1,43 @@
1+# Agent settings — "pizza": an agent that answers ONLY in JSON — the
2+# ingredients of the pizza you name, for one 30 cm pizza — with the shape
3+# declared below in `structuredOutput`. mini-me on Gemma 4 E4B, served by
4+# llmman (https://github.com/llmmanorg/llmman).
5+#
6+# llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
7+# cd demos/structured-output-pizza
8+# mm -p "Une pizza quatre fromages" | jq .
9+provider: llmman
10+model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
11+baseUrl: http://127.0.0.1:17434/v1
12+fallback: http://host.docker.internal:17434/v1
13+
14+bashTool: false
15+editTools: false
16+planTool: false
17+fetchContentTool: false
18+askUserTool: false
19+skillsDir: no-skills
20+
21+system: |
22+ You are a pizzaiolo. For the pizza the user names, list its ingredients
23+ for one 30 cm pizza.
24+
25+structuredOutput: |
26+ // One ingredient of the pizza.
27+ type Ingredient struct {
28+ Name string `json:"name"` // the ingredient's name, in the user's language
29+ Quantity float64 `json:"quantity"` // amount for one pizza
30+ Unit string `json:"unit"` // "g", "ml", "piece", "pinch" or "to taste"
31+ Category string `json:"category"` // "base", "sauce", "cheese", "meat", "vegetable", "herb" or "other"
32+ Allergens []string `json:"allergens"` // e.g. "gluten", "lactose"; empty when none
33+ Optional bool `json:"optional"`
34+ }
35+
36+ type Pizza struct {
37+ Name string `json:"name"`
38+ Vegetarian bool `json:"vegetarian"`
39+ Ingredients []Ingredient `json:"ingredients"`
40+ BakingTime *int `json:"bakingTime"` // minutes, null when unknown
41+ }
42+
43+ var Recipe Pizza
new file mode 100644
@@ -0,0 +1,43 @@
1+# Agent settings — "pizza": an agent that answers ONLY in JSON — the
2+# ingredients of the pizza you name, for one 30 cm pizza — with the shape
3+# declared below in `structuredOutput`. mini-me on Gemma 4 E4B, served by
4+# llmman (https://github.com/llmmanorg/llmman).
5+#
6+# llmman pull hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
7+# cd demos/structured-output-pizza
8+# mm -p "Une pizza quatre fromages" | jq .
9+provider: llmman
10+model: hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M
11+baseUrl: http://127.0.0.1:17434/v1
12+fallback: http://host.docker.internal:17434/v1
13+
14+bashTool: false
15+editTools: false
16+planTool: false
17+fetchContentTool: false
18+askUserTool: false
19+skillsDir: no-skills
20+
21+system: |
22+ You are a pizzaiolo. For the pizza the user names, list its ingredients
23+ for one 30 cm pizza.
24+
25+structuredOutput: |
26+ // One ingredient of the pizza.
27+ type Ingredient struct {
28+ Name string `json:"name"` // the ingredient's name, in the user's language
29+ Quantity float64 `json:"quantity"` // amount for one pizza
30+ Unit string `json:"unit"` // "g", "ml", "piece", "pinch" or "to taste"
31+ Category string `json:"category"` // "base", "sauce", "cheese", "meat", "vegetable", "herb" or "other"
32+ Allergens []string `json:"allergens"` // e.g. "gluten", "lactose"; empty when none
33+ Optional bool `json:"optional"`
34+ }
35+
36+ type Pizza struct {
37+ Name string `json:"name"`
38+ Vegetarian bool `json:"vegetarian"`
39+ Ingredients []Ingredient `json:"ingredients"`
40+ BakingTime *int `json:"bakingTime"` // minutes, null when unknown
41+ }
42+
43+ var Recipe Pizza
modified go.mod +15 -10
@@ -2,6 +2,8 @@ module mm
22
33 go 1.26.0
44
5+toolchain go1.26.9
6+
57 require (
68 github.com/firebase/genkit/go v1.13.1
79 github.com/gdamore/tcell/v2 v2.13.10
@@ -10,19 +12,22 @@ require (
1012 go.lsp.dev/jsonrpc2 v1.0.1
1113 go.lsp.dev/protocol v1.0.1
1214 go.lsp.dev/uri v1.0.1
13- golang.org/x/net v0.59.0
14- golang.org/x/text v0.42.0
15+ golang.org/x/net v0.61.0
16+ golang.org/x/text v0.43.0
1517 gopkg.in/yaml.v3 v3.0.1
1618 )
1719
1820 require (
21+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
1922 github.com/gdamore/encoding v1.0.1 // indirect
2023 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
24+ github.com/kr/text v0.2.0 // indirect
2125 github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
2226 github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
2327 github.com/rivo/uniseg v0.4.7 // indirect
28+ go.opentelemetry.io/otel/log v1.47.0 // indirect
2429 go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
25- golang.org/x/term v0.46.0 // indirect
30+ golang.org/x/term v0.47.0 // indirect
2631 )
2732
2833 require (
@@ -30,7 +35,7 @@ require (
3035 github.com/buger/jsonparser v1.1.2 // indirect
3136 github.com/coder/acp-go-sdk v0.13.5
3237 github.com/coder/websocket v1.8.14 // indirect
33- github.com/go-logr/logr v1.4.3 // indirect
38+ github.com/go-logr/logr v1.4.4 // indirect
3439 github.com/go-logr/stdr v1.2.2 // indirect
3540 github.com/goccy/go-yaml v1.19.2 // indirect
3641 github.com/google/dotprompt/go v0.0.0-20260708220100-73beb993ac95 // indirect
@@ -45,10 +50,10 @@ require (
4550 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
4651 github.com/xeipuuv/gojsonschema v1.2.0 // indirect
4752 github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
48- go.opentelemetry.io/auto/sdk v1.1.0 // indirect
49- go.opentelemetry.io/otel v1.36.0 // indirect
50- go.opentelemetry.io/otel/metric v1.36.0 // indirect
51- go.opentelemetry.io/otel/sdk v1.36.0 // indirect
52- go.opentelemetry.io/otel/trace v1.36.0 // indirect
53- golang.org/x/sys v0.48.0 // indirect
53+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
54+ go.opentelemetry.io/otel v1.47.0 // indirect
55+ go.opentelemetry.io/otel/metric v1.47.0 // indirect
56+ go.opentelemetry.io/otel/sdk v1.47.0 // indirect
57+ go.opentelemetry.io/otel/trace v1.47.0 // indirect
58+ golang.org/x/sys v0.49.0 // indirect
5459 )
@@ -2,6 +2,8 @@ module mm
2 2
3 go 1.26.03 go 1.26.0
4 4
5+toolchain go1.26.9
6+
5 require (7 require (
6 github.com/firebase/genkit/go v1.13.18 github.com/firebase/genkit/go v1.13.1
7 github.com/gdamore/tcell/v2 v2.13.109 github.com/gdamore/tcell/v2 v2.13.10
@@ -10,19 +12,22 @@ require (
10 go.lsp.dev/jsonrpc2 v1.0.112 go.lsp.dev/jsonrpc2 v1.0.1
11 go.lsp.dev/protocol v1.0.113 go.lsp.dev/protocol v1.0.1
12 go.lsp.dev/uri v1.0.114 go.lsp.dev/uri v1.0.1
13- golang.org/x/net v0.59.015+ golang.org/x/net v0.61.0
14- golang.org/x/text v0.42.016+ golang.org/x/text v0.43.0
15 gopkg.in/yaml.v3 v3.0.117 gopkg.in/yaml.v3 v3.0.1
16 )18 )
17 19
18 require (20 require (
21+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
19 github.com/gdamore/encoding v1.0.1 // indirect22 github.com/gdamore/encoding v1.0.1 // indirect
20 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect23 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
24+ github.com/kr/text v0.2.0 // indirect
21 github.com/lucasb-eyer/go-colorful v1.4.0 // indirect25 github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
22 github.com/pb33f/ordered-map/v2 v2.3.1 // indirect26 github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
23 github.com/rivo/uniseg v0.4.7 // indirect27 github.com/rivo/uniseg v0.4.7 // indirect
28+ go.opentelemetry.io/otel/log v1.47.0 // indirect
24 go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect29 go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
25- golang.org/x/term v0.46.0 // indirect30+ golang.org/x/term v0.47.0 // indirect
26 )31 )
27 32
28 require (33 require (
@@ -30,7 +35,7 @@ require (
30 github.com/buger/jsonparser v1.1.2 // indirect35 github.com/buger/jsonparser v1.1.2 // indirect
31 github.com/coder/acp-go-sdk v0.13.536 github.com/coder/acp-go-sdk v0.13.5
32 github.com/coder/websocket v1.8.14 // indirect37 github.com/coder/websocket v1.8.14 // indirect
33- github.com/go-logr/logr v1.4.3 // indirect38+ github.com/go-logr/logr v1.4.4 // indirect
34 github.com/go-logr/stdr v1.2.2 // indirect39 github.com/go-logr/stdr v1.2.2 // indirect
35 github.com/goccy/go-yaml v1.19.2 // indirect40 github.com/goccy/go-yaml v1.19.2 // indirect
36 github.com/google/dotprompt/go v0.0.0-20260708220100-73beb993ac95 // indirect41 github.com/google/dotprompt/go v0.0.0-20260708220100-73beb993ac95 // indirect
@@ -45,10 +50,10 @@ require (
45 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect50 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
46 github.com/xeipuuv/gojsonschema v1.2.0 // indirect51 github.com/xeipuuv/gojsonschema v1.2.0 // indirect
47 github.com/yosida95/uritemplate/v3 v3.0.2 // indirect52 github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
48- go.opentelemetry.io/auto/sdk v1.1.0 // indirect53+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
49- go.opentelemetry.io/otel v1.36.0 // indirect54+ go.opentelemetry.io/otel v1.47.0 // indirect
50- go.opentelemetry.io/otel/metric v1.36.0 // indirect55+ go.opentelemetry.io/otel/metric v1.47.0 // indirect
51- go.opentelemetry.io/otel/sdk v1.36.0 // indirect56+ go.opentelemetry.io/otel/sdk v1.47.0 // indirect
52- go.opentelemetry.io/otel/trace v1.36.0 // indirect57+ go.opentelemetry.io/otel/trace v1.47.0 // indirect
53- golang.org/x/sys v0.48.0 // indirect58+ golang.org/x/sys v0.49.0 // indirect
54 )59 )
modified go.sum +35 -29
@@ -2,13 +2,14 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
22 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
33 github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
44 github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
5+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
6+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
57 github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9ASYk=
68 github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=
79 github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
810 github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
11+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
912 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1213 github.com/firebase/genkit/go v1.13.1 h1:6fgQ0ogxG+SIgtYQD/yf8T0+39lacioB5voFdFYk1tI=
1314 github.com/firebase/genkit/go v1.13.1/go.mod h1:nWewix7d2O+oikJ035XPmY4mrO2phXwmk6NekKmaDLU=
1415 github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
@@ -18,8 +19,8 @@ github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1
1819 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
1920 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
2021 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
21-github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
22-github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
22+github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
23+github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
2324 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
2425 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
2526 github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
@@ -44,7 +45,6 @@ github.com/openai/openai-go v1.8.2 h1:UqSkJ1vCOPUpz9Ka5tS0324EJFEuOvMc+lA/EarJWP
4445 github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
4546 github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
4647 github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
47-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4848 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4949 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
5050 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -54,8 +54,8 @@ github.com/sashabaranov/go-openai v1.42.1 h1:9nK2UgDVVSIyoEUNDeWqu3Ttj8EqCO6FT8H
5454 github.com/sashabaranov/go-openai v1.42.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
5555 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
5656 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
57-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
58-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
57+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
58+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
5959 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
6060 github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
6161 github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -82,18 +82,24 @@ go.lsp.dev/protocol v1.0.1 h1:kLWUIk1v/deXxo1D0VO4avjZnfeuZ82mouJgJd/NbdU=
8282 go.lsp.dev/protocol v1.0.1/go.mod h1:e//i41aEqc9ml32xiNuneUwbjOjuvTA4+uv+AkssJgk=
8383 go.lsp.dev/uri v1.0.1 h1:ywxJN1UXoQ+v0I9lkoQNMmsm7lub5G9ZG7KXT8YTx30=
8484 go.lsp.dev/uri v1.0.1/go.mod h1:06a0ghafs4PuqkLsJGUmlgPHw6+kH79edRXjOT/rA8s=
85-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
86-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
87-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
88-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
89-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
90-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
91-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
92-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
93-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
94-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
85+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
86+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
87+go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
88+go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
89+go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
90+go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
91+go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
92+go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
93+go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
94+go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
95+go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
96+go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
97+go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
98+go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
9599 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
96100 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
101+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
102+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
97103 go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
98104 go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
99105 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -106,39 +112,39 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
106112 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
107113 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
108114 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
109-golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
110-golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
115+golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
116+golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
111117 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
112118 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
113119 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
114-golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
115-golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
120+golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
121+golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
116122 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
117123 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
118124 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
119125 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
120126 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
121127 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
122-golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
123-golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
128+golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
129+golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
124130 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
125131 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
126132 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
127-golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
128-golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
133+golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
134+golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
129135 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
130136 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
131137 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
132138 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
133139 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
134-golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
135-golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
140+golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
141+golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
136142 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
137143 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
138144 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
139145 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
140-golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
141-golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
146+golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
147+golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
142148 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
143149 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
144150 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -2,13 +2,14 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
2 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=2 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
3 github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=3 github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
4 github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=4 github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
5+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
6+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
5 github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9ASYk=7 github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9ASYk=
6 github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=8 github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko=
7 github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=9 github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
8 github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=10 github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
11+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
9 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=12 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 github.com/firebase/genkit/go v1.13.1 h1:6fgQ0ogxG+SIgtYQD/yf8T0+39lacioB5voFdFYk1tI=13 github.com/firebase/genkit/go v1.13.1 h1:6fgQ0ogxG+SIgtYQD/yf8T0+39lacioB5voFdFYk1tI=
13 github.com/firebase/genkit/go v1.13.1/go.mod h1:nWewix7d2O+oikJ035XPmY4mrO2phXwmk6NekKmaDLU=14 github.com/firebase/genkit/go v1.13.1/go.mod h1:nWewix7d2O+oikJ035XPmY4mrO2phXwmk6NekKmaDLU=
14 github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=15 github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
@@ -18,8 +19,8 @@ github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1
18 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=19 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
19 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=20 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
20 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=21 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
21-github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=22+github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
22-github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=23+github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
23 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=24 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
24 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=25 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
25 github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=26 github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
@@ -44,7 +45,6 @@ github.com/openai/openai-go v1.8.2 h1:UqSkJ1vCOPUpz9Ka5tS0324EJFEuOvMc+lA/EarJWP
44 github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=45 github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
45 github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=46 github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
46 github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=47 github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
47-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
48 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=48 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
49 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=49 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
50 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=50 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -54,8 +54,8 @@ github.com/sashabaranov/go-openai v1.42.1 h1:9nK2UgDVVSIyoEUNDeWqu3Ttj8EqCO6FT8H
54 github.com/sashabaranov/go-openai v1.42.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=54 github.com/sashabaranov/go-openai v1.42.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
55 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=55 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
56 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=56 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
57-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=57+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
58-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=58+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
59 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=59 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
60 github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=60 github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
61 github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=61 github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -82,18 +82,24 @@ go.lsp.dev/protocol v1.0.1 h1:kLWUIk1v/deXxo1D0VO4avjZnfeuZ82mouJgJd/NbdU=
82 go.lsp.dev/protocol v1.0.1/go.mod h1:e//i41aEqc9ml32xiNuneUwbjOjuvTA4+uv+AkssJgk=82 go.lsp.dev/protocol v1.0.1/go.mod h1:e//i41aEqc9ml32xiNuneUwbjOjuvTA4+uv+AkssJgk=
83 go.lsp.dev/uri v1.0.1 h1:ywxJN1UXoQ+v0I9lkoQNMmsm7lub5G9ZG7KXT8YTx30=83 go.lsp.dev/uri v1.0.1 h1:ywxJN1UXoQ+v0I9lkoQNMmsm7lub5G9ZG7KXT8YTx30=
84 go.lsp.dev/uri v1.0.1/go.mod h1:06a0ghafs4PuqkLsJGUmlgPHw6+kH79edRXjOT/rA8s=84 go.lsp.dev/uri v1.0.1/go.mod h1:06a0ghafs4PuqkLsJGUmlgPHw6+kH79edRXjOT/rA8s=
85-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=85+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
86-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=86+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
87-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=87+go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
88-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=88+go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
89-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=89+go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
90-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=90+go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
91-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=91+go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
92-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=92+go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
93-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=93+go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
94-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=94+go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
95+go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
96+go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
97+go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
98+go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
95 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=99 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
96 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=100 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
101+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
102+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
97 go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=103 go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
98 go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=104 go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
99 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=105 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -106,39 +112,39 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
106 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=112 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
107 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=113 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
108 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=114 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
109-golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=115+golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
110-golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=116+golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
111 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=117 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
112 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=118 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
113 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=119 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
114-golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=120+golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
115-golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=121+golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
116 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=122 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
117 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=123 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
118 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=124 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
119 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=125 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
120 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=126 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
121 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=127 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
122-golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=128+golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
123-golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=129+golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
124 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=130 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
125 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=131 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
126 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=132 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
127-golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=133+golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
128-golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=134+golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
129 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=135 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
130 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=136 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
131 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=137 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
132 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=138 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
133 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=139 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
134-golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=140+golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
135-golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=141+golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
136 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=142 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
137 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=143 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
138 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=144 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
139 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=145 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
140-golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=146+golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
141-golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=147+golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
142 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=148 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
143 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=149 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
144 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=150 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
modified internal/agent/agent.go +0 -3
@@ -571,9 +571,6 @@ func RunPrompt(ctx context.Context, e *engine.Engine, system string, tools []ai.
571571 return
572572 }
573573
574- // Process and display the result
575- messages = session.AfterTurn(messages, before, resp, history, err)
576-
577574 // Display command stats like the regular mode
578575 turn := history[min(before, len(history)):]
579576 cmds := engine.CommandList(turn)
@@ -571,9 +571,6 @@ func RunPrompt(ctx context.Context, e *engine.Engine, system string, tools []ai.
571 return571 return
572 }572 }
573 573
574- // Process and display the result
575- messages = session.AfterTurn(messages, before, resp, history, err)
576-
577 // Display command stats like the regular mode574 // Display command stats like the regular mode
578 turn := history[min(before, len(history)):]575 turn := history[min(before, len(history)):]
579 cmds := engine.CommandList(turn)576 cmds := engine.CommandList(turn)
modified internal/agent/structured_test.go +3 -3
@@ -19,9 +19,9 @@ func fakeModel(t *testing.T, content string) *engine.Engine {
1919 t.Helper()
2020 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
2121 w.Header().Set("Content-Type", "text/event-stream")
22- fmt.Fprintf(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":%s}}]}\n\n", strconv.Quote(content))
23- fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n")
24- fmt.Fprint(w, "data: [DONE]\n\n")
22+ _, _ = fmt.Fprintf(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":%s}}]}\n\n", strconv.Quote(content))
23+ _, _ = fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n")
24+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
2525 }))
2626 t.Cleanup(srv.Close)
2727 p, err := engine.Lookup("llamacpp")
@@ -19,9 +19,9 @@ func fakeModel(t *testing.T, content string) *engine.Engine {
19 t.Helper()19 t.Helper()
20 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {20 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
21 w.Header().Set("Content-Type", "text/event-stream")21 w.Header().Set("Content-Type", "text/event-stream")
22- fmt.Fprintf(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":%s}}]}\n\n", strconv.Quote(content))22+ _, _ = fmt.Fprintf(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":%s}}]}\n\n", strconv.Quote(content))
23- fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n")23+ _, _ = fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n")
24- fmt.Fprint(w, "data: [DONE]\n\n")24+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
25 }))25 }))
26 t.Cleanup(srv.Close)26 t.Cleanup(srv.Close)
27 p, err := engine.Lookup("llamacpp")27 p, err := engine.Lookup("llamacpp")
modified internal/agentsmd/example_test.go +1 -1
@@ -12,7 +12,7 @@ import (
1212
1313 func ExampleSystem() {
1414 dir, _ := os.MkdirTemp("", "project")
15- defer os.RemoveAll(dir)
15+ defer func() { _ = os.RemoveAll(dir) }()
1616 _ = os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("Run `make test` before committing."), 0o644)
1717 config.Cfg.AgentsMd = config.AgentsMdConfig{Enabled: true, Path: "AGENTS.md"}
1818
@@ -12,7 +12,7 @@ import (
12 12
13 func ExampleSystem() {13 func ExampleSystem() {
14 dir, _ := os.MkdirTemp("", "project")14 dir, _ := os.MkdirTemp("", "project")
15- defer os.RemoveAll(dir)15+ defer func() { _ = os.RemoveAll(dir) }()
16 _ = os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("Run `make test` before committing."), 0o644)16 _ = os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("Run `make test` before committing."), 0o644)
17 config.Cfg.AgentsMd = config.AgentsMdConfig{Enabled: true, Path: "AGENTS.md"}17 config.Cfg.AgentsMd = config.AgentsMdConfig{Enabled: true, Path: "AGENTS.md"}
18 18
modified internal/ask/cli.go +7 -7
@@ -89,8 +89,8 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde
8989 if req.WithContext {
9090 label += ", with the conversation"
9191 }
92- fmt.Fprintln(stderr, label+"…")
93- p.notify = func(line string) { fmt.Fprintln(stderr, line) }
92+ _, _ = fmt.Fprintln(stderr, label+"…")
93+ p.notify = func(line string) { _, _ = fmt.Fprintln(stderr, line) }
9494 // With postAsk hooks the answer is held back until they have run: what
9595 // stdout carries is what the caller reads, and a hook may replace it.
9696 var held strings.Builder
@@ -103,10 +103,10 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde
103103 answer := res.Answer
104104 if !streamed {
105105 answer = res.Text()
106- fmt.Fprint(stdout, answer)
106+ _, _ = fmt.Fprint(stdout, answer)
107107 }
108108 if answer != "" && !strings.HasSuffix(answer, "\n") {
109- fmt.Fprintln(stdout)
109+ _, _ = fmt.Fprintln(stdout)
110110 }
111111 if err == nil && res.Answer == "" {
112112 return errors.New(req.Agent + " gave no answer")
@@ -135,14 +135,14 @@ type cliSink struct {
135135
136136 var _ ui.Sink = (*cliSink)(nil)
137137
138-func (s *cliSink) Text(text string) { fmt.Fprint(s.out, text) }
138+func (s *cliSink) Text(text string) { _, _ = fmt.Fprint(s.out, text) }
139139 func (s *cliSink) Reasoning(string) {}
140140 func (s *cliSink) ToolStart(ev ui.ToolEvent) {
141- fmt.Fprintln(s.log, "🛠️ "+ev.Title)
141+ _, _ = fmt.Fprintln(s.log, "🛠️ "+ev.Title)
142142 }
143143 func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) {
144144 if res.Failed {
145- fmt.Fprintln(s.log, " │ failed")
145+ _, _ = fmt.Fprintln(s.log, " │ failed")
146146 }
147147 }
148148 func (s *cliSink) ToolRunning(string) {}
@@ -89,8 +89,8 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde
89 if req.WithContext {89 if req.WithContext {
90 label += ", with the conversation"90 label += ", with the conversation"
91 }91 }
92- fmt.Fprintln(stderr, label+"…")92+ _, _ = fmt.Fprintln(stderr, label+"…")
93- p.notify = func(line string) { fmt.Fprintln(stderr, line) }93+ p.notify = func(line string) { _, _ = fmt.Fprintln(stderr, line) }
94 // With postAsk hooks the answer is held back until they have run: what94 // With postAsk hooks the answer is held back until they have run: what
95 // stdout carries is what the caller reads, and a hook may replace it.95 // stdout carries is what the caller reads, and a hook may replace it.
96 var held strings.Builder96 var held strings.Builder
@@ -103,10 +103,10 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde
103 answer := res.Answer103 answer := res.Answer
104 if !streamed {104 if !streamed {
105 answer = res.Text()105 answer = res.Text()
106- fmt.Fprint(stdout, answer)106+ _, _ = fmt.Fprint(stdout, answer)
107 }107 }
108 if answer != "" && !strings.HasSuffix(answer, "\n") {108 if answer != "" && !strings.HasSuffix(answer, "\n") {
109- fmt.Fprintln(stdout)109+ _, _ = fmt.Fprintln(stdout)
110 }110 }
111 if err == nil && res.Answer == "" {111 if err == nil && res.Answer == "" {
112 return errors.New(req.Agent + " gave no answer")112 return errors.New(req.Agent + " gave no answer")
@@ -135,14 +135,14 @@ type cliSink struct {
135 135
136 var _ ui.Sink = (*cliSink)(nil)136 var _ ui.Sink = (*cliSink)(nil)
137 137
138-func (s *cliSink) Text(text string) { fmt.Fprint(s.out, text) }138+func (s *cliSink) Text(text string) { _, _ = fmt.Fprint(s.out, text) }
139 func (s *cliSink) Reasoning(string) {}139 func (s *cliSink) Reasoning(string) {}
140 func (s *cliSink) ToolStart(ev ui.ToolEvent) {140 func (s *cliSink) ToolStart(ev ui.ToolEvent) {
141- fmt.Fprintln(s.log, "🛠️ "+ev.Title)141+ _, _ = fmt.Fprintln(s.log, "🛠️ "+ev.Title)
142 }142 }
143 func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) {143 func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) {
144 if res.Failed {144 if res.Failed {
145- fmt.Fprintln(s.log, " │ failed")145+ _, _ = fmt.Fprintln(s.log, " │ failed")
146 }146 }
147 }147 }
148 func (s *cliSink) ToolRunning(string) {}148 func (s *cliSink) ToolRunning(string) {}
modified internal/compact/compact_test.go +2 -2
@@ -335,9 +335,9 @@ func countSummaries(msgs []*ai.Message) int {
335335 func TestCompactThroughEngineSummarize(t *testing.T) {
336336 g := genkit.Init(context.Background())
337337 var sawTools int
338- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,
338+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
339339 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
340- func(_ context.Context, r *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
340+ func(_ context.Context, r *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
341341 sawTools = len(r.Tools)
342342 return &ai.ModelResponse{
343343 Message: ai.NewModelMessage(ai.NewTextPart("## Goal\nfrom the fake model")),
@@ -335,9 +335,9 @@ func countSummaries(msgs []*ai.Message) int {
335 func TestCompactThroughEngineSummarize(t *testing.T) {335 func TestCompactThroughEngineSummarize(t *testing.T) {
336 g := genkit.Init(context.Background())336 g := genkit.Init(context.Background())
337 var sawTools int337 var sawTools int
338- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,338+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
339 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},339 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
340- func(_ context.Context, r *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {340+ func(_ context.Context, r *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
341 sawTools = len(r.Tools)341 sawTools = len(r.Tools)
342 return &ai.ModelResponse{342 return &ai.ModelResponse{
343 Message: ai.NewModelMessage(ai.NewTextPart("## Goal\nfrom the fake model")),343 Message: ai.NewModelMessage(ai.NewTextPart("## Goal\nfrom the fake model")),
modified internal/engine/apips.go +19 -0
@@ -29,6 +29,25 @@ func (l modelList) has(id string) bool {
2929 return false
3030 }
3131
32+// probeAPIPS is the probe the Ollama-style servers share: reachable when
33+// /v1/models answers, `missing` as the warning when the model is not listed,
34+// and the served window from /api/ps when the model is loaded.
35+func probeAPIPS(b Backend, missing string) Info {
36+ var list modelList
37+ info := Info{Reachable: getJSON(b.BaseURL+"/models", &list)}
38+ if !info.Reachable {
39+ return info
40+ }
41+ if !list.has(b.Model) {
42+ info.Warnings = append(info.Warnings, missing)
43+ return info
44+ }
45+ if w := loadedWindow(b.BaseURL, b.Model); w > 0 {
46+ info.ContextWindow, info.ContextSource = w, "/api/ps"
47+ }
48+ return info
49+}
50+
3251 // loadedWindow reads /api/ps, one level above /v1, and returns the served
3352 // context of `model` — 0 when it is not loaded or the route does not answer.
3453 func loadedWindow(baseURL, model string) int {
@@ -29,6 +29,25 @@ func (l modelList) has(id string) bool {
29 return false29 return false
30 }30 }
31 31
32+// probeAPIPS is the probe the Ollama-style servers share: reachable when
33+// /v1/models answers, `missing` as the warning when the model is not listed,
34+// and the served window from /api/ps when the model is loaded.
35+func probeAPIPS(b Backend, missing string) Info {
36+ var list modelList
37+ info := Info{Reachable: getJSON(b.BaseURL+"/models", &list)}
38+ if !info.Reachable {
39+ return info
40+ }
41+ if !list.has(b.Model) {
42+ info.Warnings = append(info.Warnings, missing)
43+ return info
44+ }
45+ if w := loadedWindow(b.BaseURL, b.Model); w > 0 {
46+ info.ContextWindow, info.ContextSource = w, "/api/ps"
47+ }
48+ return info
49+}
50+
32 // loadedWindow reads /api/ps, one level above /v1, and returns the served51 // loadedWindow reads /api/ps, one level above /v1, and returns the served
33 // context of `model` — 0 when it is not loaded or the route does not answer.52 // context of `model` — 0 when it is not loaded or the route does not answer.
34 func loadedWindow(baseURL, model string) int {53 func loadedWindow(baseURL, model string) int {
modified internal/engine/engine.go +2 -2
@@ -297,7 +297,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a
297297 // Text the broken attempt already streamed is not taken back.
298298 if streamBroken(err) && ctx.Err() == nil {
299299 spinner.Stop()
300- fmt.Fprintf(ui.Out, "\n[%s — retrying this model call once]\n", streamNotice(err))
300+ _, _ = fmt.Fprintf(ui.Out, "\n[%s — retrying this model call once]\n", streamNotice(err))
301301 pl.expect()
302302 resp, err = next(ctx, params)
303303 err = withServerMessage(ctx, err)
@@ -321,7 +321,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a
321321 }
322322 // ui.Out: stderr in ACP mode, never the JSON-RPC stream.
323323 spinner.Stop()
324- fmt.Fprintf(ui.Out, "\n[tool call arguments did not parse (%s: …%s) — handing it back to the model, %d/%d]\n",
324+ _, _ = fmt.Fprintf(ui.Out, "\n[tool call arguments did not parse (%s: …%s) — handing it back to the model, %d/%d]\n",
325325 what, lastRunes(err.Error(), 60), argRetries, maxArgRetries)
326326 params.Request.Messages = append(slices.Clone(params.Request.Messages), argsRetryNote(truncated))
327327 full = slices.Clone(params.Request.Messages)
@@ -297,7 +297,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a
297 // Text the broken attempt already streamed is not taken back.297 // Text the broken attempt already streamed is not taken back.
298 if streamBroken(err) && ctx.Err() == nil {298 if streamBroken(err) && ctx.Err() == nil {
299 spinner.Stop()299 spinner.Stop()
300- fmt.Fprintf(ui.Out, "\n[%s — retrying this model call once]\n", streamNotice(err))300+ _, _ = fmt.Fprintf(ui.Out, "\n[%s — retrying this model call once]\n", streamNotice(err))
301 pl.expect()301 pl.expect()
302 resp, err = next(ctx, params)302 resp, err = next(ctx, params)
303 err = withServerMessage(ctx, err)303 err = withServerMessage(ctx, err)
@@ -321,7 +321,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a
321 }321 }
322 // ui.Out: stderr in ACP mode, never the JSON-RPC stream.322 // ui.Out: stderr in ACP mode, never the JSON-RPC stream.
323 spinner.Stop()323 spinner.Stop()
324- fmt.Fprintf(ui.Out, "\n[tool call arguments did not parse (%s: …%s) — handing it back to the model, %d/%d]\n",324+ _, _ = fmt.Fprintf(ui.Out, "\n[tool call arguments did not parse (%s: …%s) — handing it back to the model, %d/%d]\n",
325 what, lastRunes(err.Error(), 60), argRetries, maxArgRetries)325 what, lastRunes(err.Error(), 60), argRetries, maxArgRetries)
326 params.Request.Messages = append(slices.Clone(params.Request.Messages), argsRetryNote(truncated))326 params.Request.Messages = append(slices.Clone(params.Request.Messages), argsRetryNote(truncated))
327 full = slices.Clone(params.Request.Messages)327 full = slices.Clone(params.Request.Messages)
modified internal/engine/engine_test.go +4 -4
@@ -32,9 +32,9 @@ func fakeGenkit(t *testing.T, turns ...*ai.ModelResponse) (*genkit.Genkit, *int)
3232 g := genkit.Init(context.Background())
3333
3434 calls := 0
35- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,
35+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
3636 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
37- func(_ context.Context, _ *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
37+ func(_ context.Context, _ *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
3838 if calls >= len(turns) {
3939 t.Errorf("the model was called %d times for %d planned turns", calls+1, len(turns))
4040 return turns[len(turns)-1], nil
@@ -301,9 +301,9 @@ func TestSummarize(t *testing.T) {
301301 var sawTools int
302302 answers := []string{" ## Goal\nnotes\n", ""}
303303 call := 0
304- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,
304+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
305305 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
306- func(_ context.Context, r *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
306+ func(_ context.Context, r *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
307307 sawTools = len(r.Tools)
308308 a := answers[call]
309309 call++
@@ -32,9 +32,9 @@ func fakeGenkit(t *testing.T, turns ...*ai.ModelResponse) (*genkit.Genkit, *int)
32 g := genkit.Init(context.Background())32 g := genkit.Init(context.Background())
33 33
34 calls := 034 calls := 0
35- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,35+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
36 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},36 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
37- func(_ context.Context, _ *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {37+ func(_ context.Context, _ *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
38 if calls >= len(turns) {38 if calls >= len(turns) {
39 t.Errorf("the model was called %d times for %d planned turns", calls+1, len(turns))39 t.Errorf("the model was called %d times for %d planned turns", calls+1, len(turns))
40 return turns[len(turns)-1], nil40 return turns[len(turns)-1], nil
@@ -301,9 +301,9 @@ func TestSummarize(t *testing.T) {
301 var sawTools int301 var sawTools int
302 answers := []string{" ## Goal\nnotes\n", ""}302 answers := []string{" ## Goal\nnotes\n", ""}
303 call := 0303 call := 0
304- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,304+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
305 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},305 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
306- func(_ context.Context, r *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {306+ func(_ context.Context, r *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
307 sawTools = len(r.Tools)307 sawTools = len(r.Tools)
308 a := answers[call]308 a := answers[call]
309 call++309 call++
modified internal/engine/llmman.go +1 -14
@@ -16,18 +16,5 @@ import (
1616 // pulls it, which can take minutes — worth a warning, because the wait looks
1717 // like a stalled server.
1818 func probeLlmman(_ context.Context, b Backend) Info {
19- var list modelList
20- info := Info{Reachable: getJSON(b.BaseURL+"/models", &list)}
21- if !info.Reachable {
22- return info
23- }
24- if !list.has(b.Model) {
25- info.Warnings = append(info.Warnings,
26- fmt.Sprintf("llmman: model %q is not in the store — the first question will pull it (or: llmman pull %s)", b.Model, b.Model))
27- return info
28- }
29- if w := loadedWindow(b.BaseURL, b.Model); w > 0 {
30- info.ContextWindow, info.ContextSource = w, "/api/ps"
31- }
32- return info
19+ return probeAPIPS(b, fmt.Sprintf("llmman: model %q is not in the store — the first question will pull it (or: llmman pull %s)", b.Model, b.Model))
3320 }
@@ -16,18 +16,5 @@ import (
16 // pulls it, which can take minutes — worth a warning, because the wait looks16 // pulls it, which can take minutes — worth a warning, because the wait looks
17 // like a stalled server.17 // like a stalled server.
18 func probeLlmman(_ context.Context, b Backend) Info {18 func probeLlmman(_ context.Context, b Backend) Info {
19- var list modelList19+ return probeAPIPS(b, fmt.Sprintf("llmman: model %q is not in the store — the first question will pull it (or: llmman pull %s)", b.Model, b.Model))
20- info := Info{Reachable: getJSON(b.BaseURL+"/models", &list)}
21- if !info.Reachable {
22- return info
23- }
24- if !list.has(b.Model) {
25- info.Warnings = append(info.Warnings,
26- fmt.Sprintf("llmman: model %q is not in the store — the first question will pull it (or: llmman pull %s)", b.Model, b.Model))
27- return info
28- }
29- if w := loadedWindow(b.BaseURL, b.Model); w > 0 {
30- info.ContextWindow, info.ContextSource = w, "/api/ps"
31- }
32- return info
33 }20 }
modified internal/engine/ollama.go +1 -14
@@ -10,18 +10,5 @@ import (
1010 // for a loaded qwen3.5:2b). Unlike llmman, Ollama's OpenAI API does NOT pull
1111 // a missing model: the request fails with a 404, so the warning says to pull.
1212 func probeOllama(_ context.Context, b Backend) Info {
13- var list modelList
14- info := Info{Reachable: getJSON(b.BaseURL+"/models", &list)}
15- if !info.Reachable {
16- return info
17- }
18- if !list.has(b.Model) {
19- info.Warnings = append(info.Warnings,
20- fmt.Sprintf("ollama: model %q is not in the server's list — ollama pull %s", b.Model, b.Model))
21- return info
22- }
23- if w := loadedWindow(b.BaseURL, b.Model); w > 0 {
24- info.ContextWindow, info.ContextSource = w, "/api/ps"
25- }
26- return info
13+ return probeAPIPS(b, fmt.Sprintf("ollama: model %q is not in the server's list — ollama pull %s", b.Model, b.Model))
2714 }
@@ -10,18 +10,5 @@ import (
10 // for a loaded qwen3.5:2b). Unlike llmman, Ollama's OpenAI API does NOT pull10 // for a loaded qwen3.5:2b). Unlike llmman, Ollama's OpenAI API does NOT pull
11 // a missing model: the request fails with a 404, so the warning says to pull.11 // a missing model: the request fails with a 404, so the warning says to pull.
12 func probeOllama(_ context.Context, b Backend) Info {12 func probeOllama(_ context.Context, b Backend) Info {
13- var list modelList13+ return probeAPIPS(b, fmt.Sprintf("ollama: model %q is not in the server's list — ollama pull %s", b.Model, b.Model))
14- info := Info{Reachable: getJSON(b.BaseURL+"/models", &list)}
15- if !info.Reachable {
16- return info
17- }
18- if !list.has(b.Model) {
19- info.Warnings = append(info.Warnings,
20- fmt.Sprintf("ollama: model %q is not in the server's list — ollama pull %s", b.Model, b.Model))
21- return info
22- }
23- if w := loadedWindow(b.BaseURL, b.Model); w > 0 {
24- info.ContextWindow, info.ContextSource = w, "/api/ps"
25- }
26- return info
27 }14 }
modified internal/engine/openai_compat.go +2 -2
@@ -268,7 +268,7 @@ func reachable(baseURL string) bool {
268268 if err != nil {
269269 return false
270270 }
271- defer resp.Body.Close()
271+ defer func() { _ = resp.Body.Close() }()
272272 return resp.StatusCode == http.StatusOK
273273 }
274274
@@ -279,7 +279,7 @@ func getJSON(url string, out any) bool {
279279 if err != nil {
280280 return false
281281 }
282- defer resp.Body.Close()
282+ defer func() { _ = resp.Body.Close() }()
283283 if resp.StatusCode != http.StatusOK {
284284 return false
285285 }
@@ -268,7 +268,7 @@ func reachable(baseURL string) bool {
268 if err != nil {268 if err != nil {
269 return false269 return false
270 }270 }
271- defer resp.Body.Close()271+ defer func() { _ = resp.Body.Close() }()
272 return resp.StatusCode == http.StatusOK272 return resp.StatusCode == http.StatusOK
273 }273 }
274 274
@@ -279,7 +279,7 @@ func getJSON(url string, out any) bool {
279 if err != nil {279 if err != nil {
280 return false280 return false
281 }281 }
282- defer resp.Body.Close()282+ defer func() { _ = resp.Body.Close() }()
283 if resp.StatusCode != http.StatusOK {283 if resp.StatusCode != http.StatusOK {
284 return false284 return false
285 }285 }
modified internal/engine/progress.go +1 -1
@@ -146,7 +146,7 @@ func watchdog(ctx context.Context, between, firstToken time.Duration) (genCtx co
146146 spinner.Stop()
147147 // ui.Out, not stdout: in ACP mode this diagnosis belongs to
148148 // the logs (stderr), never to the JSON-RPC stream.
149- fmt.Fprintf(ui.Out, "\n[watchdog: %s after %v]\n", what, limit)
149+ _, _ = fmt.Fprintf(ui.Out, "\n[watchdog: %s after %v]\n", what, limit)
150150 cancel()
151151 return
152152 }
@@ -146,7 +146,7 @@ func watchdog(ctx context.Context, between, firstToken time.Duration) (genCtx co
146 spinner.Stop()146 spinner.Stop()
147 // ui.Out, not stdout: in ACP mode this diagnosis belongs to147 // ui.Out, not stdout: in ACP mode this diagnosis belongs to
148 // the logs (stderr), never to the JSON-RPC stream.148 // the logs (stderr), never to the JSON-RPC stream.
149- fmt.Fprintf(ui.Out, "\n[watchdog: %s after %v]\n", what, limit)149+ _, _ = fmt.Fprintf(ui.Out, "\n[watchdog: %s after %v]\n", what, limit)
150 cancel()150 cancel()
151 return151 return
152 }152 }
modified internal/engine/progress_test.go +6 -6
@@ -47,7 +47,7 @@ func timeouts(t *testing.T, between, first time.Duration) {
4747 func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgress *bool) string {
4848 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {
4949 fl := w.(http.Flusher)
50- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, ""))
50+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, ""))
5151 fl.Flush()
5252 for start := time.Now(); time.Since(start) < reading; {
5353 if every == 0 {
@@ -55,15 +55,15 @@ func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgres
5555 break
5656 }
5757 time.Sleep(every)
58- fmt.Fprint(w, `data: {"choices":[{"index":0,"delta":{"content":null}}],"prompt_progress":{"total":25000,"cache":0,"processed":1000,"time_ms":10}}`+"\n\n")
58+ _, _ = fmt.Fprint(w, `data: {"choices":[{"index":0,"delta":{"content":null}}],"prompt_progress":{"total":25000,"cache":0,"processed":1000,"time_ms":10}}`+"\n\n")
5959 fl.Flush()
6060 }
61- fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, ""))
61+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, ""))
6262 fl.Flush()
6363 time.Sleep(pause)
64- fmt.Fprint(w, sseChunkLine(`{"content":" answer"}`, ""))
65- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
66- fmt.Fprint(w, "data: [DONE]\n\n")
64+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":" answer"}`, ""))
65+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
66+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
6767 })
6868 if sawProgress != nil {
6969 // The provider must ask for it: look at what the next request sends.
@@ -47,7 +47,7 @@ func timeouts(t *testing.T, between, first time.Duration) {
47 func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgress *bool) string {47 func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgress *bool) string {
48 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {48 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {
49 fl := w.(http.Flusher)49 fl := w.(http.Flusher)
50- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, ""))50+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, ""))
51 fl.Flush()51 fl.Flush()
52 for start := time.Now(); time.Since(start) < reading; {52 for start := time.Now(); time.Since(start) < reading; {
53 if every == 0 {53 if every == 0 {
@@ -55,15 +55,15 @@ func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgres
55 break55 break
56 }56 }
57 time.Sleep(every)57 time.Sleep(every)
58- fmt.Fprint(w, `data: {"choices":[{"index":0,"delta":{"content":null}}],"prompt_progress":{"total":25000,"cache":0,"processed":1000,"time_ms":10}}`+"\n\n")58+ _, _ = fmt.Fprint(w, `data: {"choices":[{"index":0,"delta":{"content":null}}],"prompt_progress":{"total":25000,"cache":0,"processed":1000,"time_ms":10}}`+"\n\n")
59 fl.Flush()59 fl.Flush()
60 }60 }
61- fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, ""))61+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, ""))
62 fl.Flush()62 fl.Flush()
63 time.Sleep(pause)63 time.Sleep(pause)
64- fmt.Fprint(w, sseChunkLine(`{"content":" answer"}`, ""))64+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":" answer"}`, ""))
65- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))65+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
66- fmt.Fprint(w, "data: [DONE]\n\n")66+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
67 })67 })
68 if sawProgress != nil {68 if sawProgress != nil {
69 // The provider must ask for it: look at what the next request sends.69 // The provider must ask for it: look at what the next request sends.
modified internal/engine/reasoning_test.go +9 -9
@@ -110,7 +110,7 @@ func TestReasoningTapIsScopedToItsRequest(t *testing.T) {
110110 t.Errorf("%s: %v", who, err)
111111 return
112112 }
113- defer resp.Body.Close()
113+ defer func() { _ = resp.Body.Close() }()
114114 _, _ = io.ReadAll(resp.Body)
115115 }
116116
@@ -175,10 +175,10 @@ var _ = option.WithMiddleware(reasoningTap) // the middleware still fits the opt
175175 // request.
176176 func TestReasoningStaysOutOfTheHistory(t *testing.T) {
177177 srv, bodies := recordingLlama(t, func(w http.ResponseWriter) {
178- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"let me think"}`, ""))
179- fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
180- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
181- fmt.Fprint(w, "data: [DONE]\n\n")
178+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"let me think"}`, ""))
179+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
180+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
181+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
182182 })
183183 e := engineOn(t, srv.URL+"/v1")
184184
@@ -213,13 +213,13 @@ func TestReasoningStreamsAsOneBlock(t *testing.T) {
213213 // the tap reads them all before Genkit's callback runs and the bug
214214 // does not show.
215215 for _, word := range []string{"The", " user", " asks"} {
216- fmt.Fprint(w, sseChunkLine(fmt.Sprintf(`{"reasoning_content":%q}`, word), ""))
216+ _, _ = fmt.Fprint(w, sseChunkLine(fmt.Sprintf(`{"reasoning_content":%q}`, word), ""))
217217 w.(http.Flusher).Flush()
218218 time.Sleep(20 * time.Millisecond)
219219 }
220- fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
221- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
222- fmt.Fprint(w, "data: [DONE]\n\n")
220+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
221+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
222+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
223223 })
224224 e := engineOn(t, srv.URL+"/v1")
225225 var out strings.Builder
@@ -110,7 +110,7 @@ func TestReasoningTapIsScopedToItsRequest(t *testing.T) {
110 t.Errorf("%s: %v", who, err)110 t.Errorf("%s: %v", who, err)
111 return111 return
112 }112 }
113- defer resp.Body.Close()113+ defer func() { _ = resp.Body.Close() }()
114 _, _ = io.ReadAll(resp.Body)114 _, _ = io.ReadAll(resp.Body)
115 }115 }
116 116
@@ -175,10 +175,10 @@ var _ = option.WithMiddleware(reasoningTap) // the middleware still fits the opt
175 // request.175 // request.
176 func TestReasoningStaysOutOfTheHistory(t *testing.T) {176 func TestReasoningStaysOutOfTheHistory(t *testing.T) {
177 srv, bodies := recordingLlama(t, func(w http.ResponseWriter) {177 srv, bodies := recordingLlama(t, func(w http.ResponseWriter) {
178- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"let me think"}`, ""))178+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"let me think"}`, ""))
179- fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))179+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
180- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))180+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
181- fmt.Fprint(w, "data: [DONE]\n\n")181+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
182 })182 })
183 e := engineOn(t, srv.URL+"/v1")183 e := engineOn(t, srv.URL+"/v1")
184 184
@@ -213,13 +213,13 @@ func TestReasoningStreamsAsOneBlock(t *testing.T) {
213 // the tap reads them all before Genkit's callback runs and the bug213 // the tap reads them all before Genkit's callback runs and the bug
214 // does not show.214 // does not show.
215 for _, word := range []string{"The", " user", " asks"} {215 for _, word := range []string{"The", " user", " asks"} {
216- fmt.Fprint(w, sseChunkLine(fmt.Sprintf(`{"reasoning_content":%q}`, word), ""))216+ _, _ = fmt.Fprint(w, sseChunkLine(fmt.Sprintf(`{"reasoning_content":%q}`, word), ""))
217 w.(http.Flusher).Flush()217 w.(http.Flusher).Flush()
218 time.Sleep(20 * time.Millisecond)218 time.Sleep(20 * time.Millisecond)
219 }219 }
220- fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))220+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
221- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))221+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
222- fmt.Fprint(w, "data: [DONE]\n\n")222+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
223 })223 })
224 e := engineOn(t, srv.URL+"/v1")224 e := engineOn(t, srv.URL+"/v1")
225 var out strings.Builder225 var out strings.Builder
modified internal/engine/servererr_test.go +7 -7
@@ -38,16 +38,16 @@ func fakeLlama(t *testing.T, answer func(call int, w http.ResponseWriter)) (*htt
3838 }
3939
4040 func goodAnswer(w http.ResponseWriter) {
41- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"all good"}`, ""))
42- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
43- fmt.Fprint(w, "data: [DONE]\n\n")
41+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"all good"}`, ""))
42+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
43+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
4444 }
4545
4646 // llama.cpp's way of failing mid-stream: an `error:` event, after 200 and
4747 // some content.
4848 func errorEvent(w http.ResponseWriter) {
49- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
50- fmt.Fprint(w, `error: {"code":500,"message":"Failed to parse input at pos 12","type":"server_error"}`+"\n\n")
49+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
50+ _, _ = fmt.Fprint(w, `error: {"code":500,"message":"Failed to parse input at pos 12","type":"server_error"}`+"\n\n")
5151 }
5252
5353 func engineOn(t *testing.T, baseURL string) *Engine {
@@ -101,10 +101,10 @@ func TestServerErrorEventIsExplained(t *testing.T) {
101101 // line that says what happened.
102102 func TestCutStreamIsExplained(t *testing.T) {
103103 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {
104- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
104+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
105105 w.(http.Flusher).Flush()
106106 conn, _, _ := w.(http.Hijacker).Hijack()
107- conn.Close()
107+ _ = conn.Close()
108108 })
109109 e := engineOn(t, srv.URL+"/v1")
110110 _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("hi")}, nil)
@@ -38,16 +38,16 @@ func fakeLlama(t *testing.T, answer func(call int, w http.ResponseWriter)) (*htt
38 }38 }
39 39
40 func goodAnswer(w http.ResponseWriter) {40 func goodAnswer(w http.ResponseWriter) {
41- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"all good"}`, ""))41+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"all good"}`, ""))
42- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))42+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
43- fmt.Fprint(w, "data: [DONE]\n\n")43+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
44 }44 }
45 45
46 // llama.cpp's way of failing mid-stream: an `error:` event, after 200 and46 // llama.cpp's way of failing mid-stream: an `error:` event, after 200 and
47 // some content.47 // some content.
48 func errorEvent(w http.ResponseWriter) {48 func errorEvent(w http.ResponseWriter) {
49- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))49+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
50- fmt.Fprint(w, `error: {"code":500,"message":"Failed to parse input at pos 12","type":"server_error"}`+"\n\n")50+ _, _ = fmt.Fprint(w, `error: {"code":500,"message":"Failed to parse input at pos 12","type":"server_error"}`+"\n\n")
51 }51 }
52 52
53 func engineOn(t *testing.T, baseURL string) *Engine {53 func engineOn(t *testing.T, baseURL string) *Engine {
@@ -101,10 +101,10 @@ func TestServerErrorEventIsExplained(t *testing.T) {
101 // line that says what happened.101 // line that says what happened.
102 func TestCutStreamIsExplained(t *testing.T) {102 func TestCutStreamIsExplained(t *testing.T) {
103 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {103 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {
104- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))104+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
105 w.(http.Flusher).Flush()105 w.(http.Flusher).Flush()
106 conn, _, _ := w.(http.Hijacker).Hijack()106 conn, _, _ := w.(http.Hijacker).Hijack()
107- conn.Close()107+ _ = conn.Close()
108 })108 })
109 e := engineOn(t, srv.URL+"/v1")109 e := engineOn(t, srv.URL+"/v1")
110 _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("hi")}, nil)110 _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("hi")}, nil)
modified internal/engine/structured_test.go +6 -6
@@ -31,9 +31,9 @@ func withOutputSchema(t *testing.T, src string) {
3131 // answering streams content as the model's whole answer.
3232 func answering(content string) func(http.ResponseWriter) {
3333 return func(w http.ResponseWriter) {
34- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":`+strconv.Quote(content)+`}`, ""))
35- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
36- fmt.Fprint(w, "data: [DONE]\n\n")
34+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":`+strconv.Quote(content)+`}`, ""))
35+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
36+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
3737 }
3838 }
3939
@@ -140,9 +140,9 @@ func TestPropertiesReachTheServerInDeclarationOrder(t *testing.T) {
140140 func TestACutStructuredAnswerNamesMaxTokens(t *testing.T) {
141141 withOutputSchema(t, "var Names []string")
142142 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {
143- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"[\"Ada\",\"Gra"}`, ""))
144- fmt.Fprint(w, sseChunkLine(`{}`, "length"))
145- fmt.Fprint(w, "data: [DONE]\n\n")
143+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"[\"Ada\",\"Gra"}`, ""))
144+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "length"))
145+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
146146 })
147147 _, _, err := engineOn(t, srv.URL+"/v1").Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("?")}, nil)
148148 if err == nil || !strings.Contains(err.Error(), "cut off by max_tokens") {
@@ -31,9 +31,9 @@ func withOutputSchema(t *testing.T, src string) {
31 // answering streams content as the model's whole answer.31 // answering streams content as the model's whole answer.
32 func answering(content string) func(http.ResponseWriter) {32 func answering(content string) func(http.ResponseWriter) {
33 return func(w http.ResponseWriter) {33 return func(w http.ResponseWriter) {
34- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":`+strconv.Quote(content)+`}`, ""))34+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":`+strconv.Quote(content)+`}`, ""))
35- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))35+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
36- fmt.Fprint(w, "data: [DONE]\n\n")36+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
37 }37 }
38 }38 }
39 39
@@ -140,9 +140,9 @@ func TestPropertiesReachTheServerInDeclarationOrder(t *testing.T) {
140 func TestACutStructuredAnswerNamesMaxTokens(t *testing.T) {140 func TestACutStructuredAnswerNamesMaxTokens(t *testing.T) {
141 withOutputSchema(t, "var Names []string")141 withOutputSchema(t, "var Names []string")
142 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {142 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {
143- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"[\"Ada\",\"Gra"}`, ""))143+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"[\"Ada\",\"Gra"}`, ""))
144- fmt.Fprint(w, sseChunkLine(`{}`, "length"))144+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "length"))
145- fmt.Fprint(w, "data: [DONE]\n\n")145+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
146 })146 })
147 _, _, err := engineOn(t, srv.URL+"/v1").Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("?")}, nil)147 _, _, err := engineOn(t, srv.URL+"/v1").Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("?")}, nil)
148 if err == nil || !strings.Contains(err.Error(), "cut off by max_tokens") {148 if err == nil || !strings.Contains(err.Error(), "cut off by max_tokens") {
modified internal/engine/summary_test.go +3 -3
@@ -106,9 +106,9 @@ func TestSummaryTurnsReasoningOffOverSampling(t *testing.T) {
106106 // and names the setting — the session file only ever said "empty summary".
107107 func TestEmptySummarySaysWhy(t *testing.T) {
108108 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {
109- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"Let me think about the session..."}`, ""))
110- fmt.Fprint(w, sseChunkLine(`{}`, "length"))
111- fmt.Fprint(w, "data: [DONE]\n\n")
109+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"Let me think about the session..."}`, ""))
110+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "length"))
111+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
112112 })
113113 e := engineOn(t, srv.URL+"/v1")
114114
@@ -106,9 +106,9 @@ func TestSummaryTurnsReasoningOffOverSampling(t *testing.T) {
106 // and names the setting — the session file only ever said "empty summary".106 // and names the setting — the session file only ever said "empty summary".
107 func TestEmptySummarySaysWhy(t *testing.T) {107 func TestEmptySummarySaysWhy(t *testing.T) {
108 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {108 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {
109- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"Let me think about the session..."}`, ""))109+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"Let me think about the session..."}`, ""))
110- fmt.Fprint(w, sseChunkLine(`{}`, "length"))110+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "length"))
111- fmt.Fprint(w, "data: [DONE]\n\n")111+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
112 })112 })
113 e := engineOn(t, srv.URL+"/v1")113 e := engineOn(t, srv.URL+"/v1")
114 114
modified internal/engine/thoughts.go +3 -3
@@ -41,10 +41,10 @@ func (t *thoughts) emit(text string) {
4141 // without it the marker lands on the spinner's line and the next
4242 // frame erases it.
4343 spinner.Stop()
44- fmt.Fprint(ui.Out, "\n💭 ")
44+ _, _ = fmt.Fprint(ui.Out, "\n💭 ")
4545 t.open = true
4646 }
47- fmt.Fprint(ui.Out, dim(text))
47+ _, _ = fmt.Fprint(ui.Out, dim(text))
4848 }
4949
5050 // done ends the block: a newline, so the answer starts on a line of its own.
@@ -60,7 +60,7 @@ func (t *thoughts) done() {
6060 return
6161 }
6262 t.open = false
63- fmt.Fprintln(ui.Out)
63+ _, _ = fmt.Fprintln(ui.Out)
6464 }
6565
6666 // dim renders reasoning as dim italic — a different colour and a different
@@ -41,10 +41,10 @@ func (t *thoughts) emit(text string) {
41 // without it the marker lands on the spinner's line and the next41 // without it the marker lands on the spinner's line and the next
42 // frame erases it.42 // frame erases it.
43 spinner.Stop()43 spinner.Stop()
44- fmt.Fprint(ui.Out, "\n💭 ")44+ _, _ = fmt.Fprint(ui.Out, "\n💭 ")
45 t.open = true45 t.open = true
46 }46 }
47- fmt.Fprint(ui.Out, dim(text))47+ _, _ = fmt.Fprint(ui.Out, dim(text))
48 }48 }
49 49
50 // done ends the block: a newline, so the answer starts on a line of its own.50 // done ends the block: a newline, so the answer starts on a line of its own.
@@ -60,7 +60,7 @@ func (t *thoughts) done() {
60 return60 return
61 }61 }
62 t.open = false62 t.open = false
63- fmt.Fprintln(ui.Out)63+ _, _ = fmt.Fprintln(ui.Out)
64 }64 }
65 65
66 // dim renders reasoning as dim italic — a different colour and a different66 // dim renders reasoning as dim italic — a different colour and a different
modified internal/engine/toolargs_test.go +7 -7
@@ -14,7 +14,7 @@ import (
1414
1515 // The plugin's error for a tool call cut off by max_tokens, as it was seen on
1616 // 2026-09-26 (shortened: the real one quoted the whole document).
17-var truncatedArgs = errors.New(`could not parse tool args: unmarshal failed to parse json string {"path":"docs/M.md","content":"# Mémoire: unexpected end of JSON input`)
17+var errTruncatedArgs = errors.New(`could not parse tool args: unmarshal failed to parse json string {"path":"docs/M.md","content":"# Mémoire: unexpected end of JSON input`)
1818
1919 // scriptedGenkit registers a model that plays `steps` in order — a response
2020 // or an error — and records every request it was sent, plus the fake `bash`.
@@ -22,9 +22,9 @@ func scriptedGenkit(t *testing.T, steps ...any) (*genkit.Genkit, *[]*ai.ModelReq
2222 t.Helper()
2323 g := genkit.Init(context.Background())
2424 var seen []*ai.ModelRequest
25- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,
25+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
2626 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
27- func(_ context.Context, req *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
27+ func(_ context.Context, req *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
2828 seen = append(seen, req)
2929 if len(seen) > len(steps) {
3030 t.Fatalf("the model was called %d times for %d planned steps", len(seen), len(steps))
@@ -51,7 +51,7 @@ func scriptedGenkit(t *testing.T, steps ...any) (*genkit.Genkit, *[]*ai.ModelReq
5151 // front end keeps, where the model will read it again for the next piece.
5252 func TestTruncatedToolCallIsHandedBack(t *testing.T) {
5353 bash := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "bash", Input: map[string]any{"command": "ls"}, Ref: "1"}))
54- g, seen := scriptedGenkit(t, bash, truncatedArgs, modelTurn(ai.NewTextPart("written in two parts")))
54+ g, seen := scriptedGenkit(t, bash, errTruncatedArgs, modelTurn(ai.NewTextPart("written in two parts")))
5555
5656 resp, full, err := testEngine(g).Generate(context.Background(),
5757 []*ai.Message{ai.NewUserTextMessage("write the document")},
@@ -92,9 +92,9 @@ func TestTruncatedToolCallGivesUpAndExplains(t *testing.T) {
9292 config.Cfg.Sampling = map[string]any{"max_tokens": 4096}
9393 defer func() { config.Cfg.Sampling = old }()
9494
95- steps := []any{truncatedArgs}
95+ steps := []any{errTruncatedArgs}
9696 for range maxArgRetries {
97- steps = append(steps, truncatedArgs)
97+ steps = append(steps, errTruncatedArgs)
9898 }
9999 g, seen := scriptedGenkit(t, steps...)
100100 e := testEngine(g)
@@ -120,7 +120,7 @@ func TestArgsError(t *testing.T) {
120120 }{
121121 {nil, false, false},
122122 {errors.New("connection refused"), false, false},
123- {truncatedArgs, true, false},
123+ {errTruncatedArgs, true, false},
124124 {errors.New(`could not parse tool args: unmarshal failed to parse json string {"a":1}}: invalid character '}'`), false, true},
125125 }
126126 for _, c := range cases {
@@ -14,7 +14,7 @@ import (
14 14
15 // The plugin's error for a tool call cut off by max_tokens, as it was seen on15 // The plugin's error for a tool call cut off by max_tokens, as it was seen on
16 // 2026-09-26 (shortened: the real one quoted the whole document).16 // 2026-09-26 (shortened: the real one quoted the whole document).
17-var truncatedArgs = errors.New(`could not parse tool args: unmarshal failed to parse json string {"path":"docs/M.md","content":"# Mémoire: unexpected end of JSON input`)17+var errTruncatedArgs = errors.New(`could not parse tool args: unmarshal failed to parse json string {"path":"docs/M.md","content":"# Mémoire: unexpected end of JSON input`)
18 18
19 // scriptedGenkit registers a model that plays `steps` in order — a response19 // scriptedGenkit registers a model that plays `steps` in order — a response
20 // or an error — and records every request it was sent, plus the fake `bash`.20 // or an error — and records every request it was sent, plus the fake `bash`.
@@ -22,9 +22,9 @@ func scriptedGenkit(t *testing.T, steps ...any) (*genkit.Genkit, *[]*ai.ModelReq
22 t.Helper()22 t.Helper()
23 g := genkit.Init(context.Background())23 g := genkit.Init(context.Background())
24 var seen []*ai.ModelRequest24 var seen []*ai.ModelRequest
25- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,25+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
26 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},26 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
27- func(_ context.Context, req *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {27+ func(_ context.Context, req *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
28 seen = append(seen, req)28 seen = append(seen, req)
29 if len(seen) > len(steps) {29 if len(seen) > len(steps) {
30 t.Fatalf("the model was called %d times for %d planned steps", len(seen), len(steps))30 t.Fatalf("the model was called %d times for %d planned steps", len(seen), len(steps))
@@ -51,7 +51,7 @@ func scriptedGenkit(t *testing.T, steps ...any) (*genkit.Genkit, *[]*ai.ModelReq
51 // front end keeps, where the model will read it again for the next piece.51 // front end keeps, where the model will read it again for the next piece.
52 func TestTruncatedToolCallIsHandedBack(t *testing.T) {52 func TestTruncatedToolCallIsHandedBack(t *testing.T) {
53 bash := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "bash", Input: map[string]any{"command": "ls"}, Ref: "1"}))53 bash := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "bash", Input: map[string]any{"command": "ls"}, Ref: "1"}))
54- g, seen := scriptedGenkit(t, bash, truncatedArgs, modelTurn(ai.NewTextPart("written in two parts")))54+ g, seen := scriptedGenkit(t, bash, errTruncatedArgs, modelTurn(ai.NewTextPart("written in two parts")))
55 55
56 resp, full, err := testEngine(g).Generate(context.Background(),56 resp, full, err := testEngine(g).Generate(context.Background(),
57 []*ai.Message{ai.NewUserTextMessage("write the document")},57 []*ai.Message{ai.NewUserTextMessage("write the document")},
@@ -92,9 +92,9 @@ func TestTruncatedToolCallGivesUpAndExplains(t *testing.T) {
92 config.Cfg.Sampling = map[string]any{"max_tokens": 4096}92 config.Cfg.Sampling = map[string]any{"max_tokens": 4096}
93 defer func() { config.Cfg.Sampling = old }()93 defer func() { config.Cfg.Sampling = old }()
94 94
95- steps := []any{truncatedArgs}95+ steps := []any{errTruncatedArgs}
96 for range maxArgRetries {96 for range maxArgRetries {
97- steps = append(steps, truncatedArgs)97+ steps = append(steps, errTruncatedArgs)
98 }98 }
99 g, seen := scriptedGenkit(t, steps...)99 g, seen := scriptedGenkit(t, steps...)
100 e := testEngine(g)100 e := testEngine(g)
@@ -120,7 +120,7 @@ func TestArgsError(t *testing.T) {
120 }{120 }{
121 {nil, false, false},121 {nil, false, false},
122 {errors.New("connection refused"), false, false},122 {errors.New("connection refused"), false, false},
123- {truncatedArgs, true, false},123+ {errTruncatedArgs, true, false},
124 {errors.New(`could not parse tool args: unmarshal failed to parse json string {"a":1}}: invalid character '}'`), false, true},124 {errors.New(`could not parse tool args: unmarshal failed to parse json string {"a":1}}: invalid character '}'`), false, true},
125 }125 }
126 for _, c := range cases {126 for _, c := range cases {
modified internal/engine/trace.go +3 -1
@@ -51,7 +51,9 @@ func traceMiddleware(dir, agent string) option.Middleware {
5151 where := traceDirOf(dir)
5252 base := filepath.Join(where, fmt.Sprintf("%s-%s-%04d", time.Now().Format("20060102-150405.000"), agent, traceSeq.Add(1)))
5353 if err := os.MkdirAll(where, 0o755); err != nil {
54- traceWarned.Do(func() { fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err) })
54+ traceWarned.Do(func() {
55+ _, _ = fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err)
56+ })
5557 }
5658 if req.Body != nil {
5759 if body, err := io.ReadAll(req.Body); err == nil {
@@ -51,7 +51,9 @@ func traceMiddleware(dir, agent string) option.Middleware {
51 where := traceDirOf(dir)51 where := traceDirOf(dir)
52 base := filepath.Join(where, fmt.Sprintf("%s-%s-%04d", time.Now().Format("20060102-150405.000"), agent, traceSeq.Add(1)))52 base := filepath.Join(where, fmt.Sprintf("%s-%s-%04d", time.Now().Format("20060102-150405.000"), agent, traceSeq.Add(1)))
53 if err := os.MkdirAll(where, 0o755); err != nil {53 if err := os.MkdirAll(where, 0o755); err != nil {
54- traceWarned.Do(func() { fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err) })54+ traceWarned.Do(func() {
55+ _, _ = fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err)
56+ })
55 }57 }
56 if req.Body != nil {58 if req.Body != nil {
57 if body, err := io.ReadAll(req.Body); err == nil {59 if body, err := io.ReadAll(req.Body); err == nil {
modified internal/eval/routing_test.go +1 -1
@@ -116,7 +116,7 @@ func TestSkillRouting(t *testing.T) {
116116 if devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil {
117117 saved := os.Stdout
118118 os.Stdout = devnull
119- t.Cleanup(func() { os.Stdout = saved; devnull.Close() })
119+ t.Cleanup(func() { os.Stdout = saved; _ = devnull.Close() })
120120 }
121121
122122 path, err := config.Load(os.Getenv("AGENT_CONFIG"))
@@ -116,7 +116,7 @@ func TestSkillRouting(t *testing.T) {
116 if devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil {116 if devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil {
117 saved := os.Stdout117 saved := os.Stdout
118 os.Stdout = devnull118 os.Stdout = devnull
119- t.Cleanup(func() { os.Stdout = saved; devnull.Close() })119+ t.Cleanup(func() { os.Stdout = saved; _ = devnull.Close() })
120 }120 }
121 121
122 path, err := config.Load(os.Getenv("AGENT_CONFIG"))122 path, err := config.Load(os.Getenv("AGENT_CONFIG"))
modified internal/fileedit/fileedit_test.go +5 -2
@@ -296,8 +296,11 @@ func TestUnifiedMatchesGitApply(t *testing.T) {
296296 }
297297 }
298298 run("init", "-q")
299- os.WriteFile(filepath.Join(dir, "f.txt"), []byte(tc.old), 0o644)
300- os.WriteFile(filepath.Join(dir, "p.diff"), []byte(patch), 0o644)
299+ for name, body := range map[string]string{"f.txt": tc.old, "p.diff": patch} {
300+ if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
301+ t.Fatal(err)
302+ }
303+ }
301304 run("apply", "p.diff")
302305 if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new {
303306 t.Errorf("after apply: %q, want %q\npatch:\n%s", got, tc.new, patch)
@@ -296,8 +296,11 @@ func TestUnifiedMatchesGitApply(t *testing.T) {
296 }296 }
297 }297 }
298 run("init", "-q")298 run("init", "-q")
299- os.WriteFile(filepath.Join(dir, "f.txt"), []byte(tc.old), 0o644)299+ for name, body := range map[string]string{"f.txt": tc.old, "p.diff": patch} {
300- os.WriteFile(filepath.Join(dir, "p.diff"), []byte(patch), 0o644)300+ if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
301+ t.Fatal(err)
302+ }
303+ }
301 run("apply", "p.diff")304 run("apply", "p.diff")
302 if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new {305 if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new {
303 t.Errorf("after apply: %q, want %q\npatch:\n%s", got, tc.new, patch)306 t.Errorf("after apply: %q, want %q\npatch:\n%s", got, tc.new, patch)
modified internal/fileedit/textfile.go +2 -2
@@ -100,10 +100,10 @@ func (f *file) write(body string) error {
100100 if err != nil {
101101 return err
102102 }
103- defer os.Remove(tmp.Name()) // no effect once the rename succeeded
103+ defer func() { _ = os.Remove(tmp.Name()) }() // no effect once the rename succeeded
104104
105105 if _, err := tmp.WriteString(f.render(body)); err != nil {
106- tmp.Close()
106+ _ = tmp.Close() // the write error is the one worth reporting
107107 return err
108108 }
109109 if err := tmp.Close(); err != nil {
@@ -100,10 +100,10 @@ func (f *file) write(body string) error {
100 if err != nil {100 if err != nil {
101 return err101 return err
102 }102 }
103- defer os.Remove(tmp.Name()) // no effect once the rename succeeded103+ defer func() { _ = os.Remove(tmp.Name()) }() // no effect once the rename succeeded
104 104
105 if _, err := tmp.WriteString(f.render(body)); err != nil {105 if _, err := tmp.WriteString(f.render(body)); err != nil {
106- tmp.Close()106+ _ = tmp.Close() // the write error is the one worth reporting
107 return err107 return err
108 }108 }
109 if err := tmp.Close(); err != nil {109 if err := tmp.Close(); err != nil {
modified internal/lsp/format.go +3 -3
@@ -82,7 +82,7 @@ func hoverText(c protocol.HoverContents) string {
8282 }
8383 case protocol.String:
8484 text = string(v)
85- case *protocol.MarkedStringWithLanguage:
85+ case *protocol.MarkedStringWithLanguage: //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
8686 if v != nil {
8787 text = v.Value
8888 }
@@ -97,11 +97,11 @@ func hoverText(c protocol.HoverContents) string {
9797 }
9898
9999 // markedString is one arm of the deprecated MarkedString union.
100-func markedString(m protocol.MarkedString) string {
100+func markedString(m protocol.MarkedString) string { //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
101101 switch v := m.(type) {
102102 case protocol.String:
103103 return string(v)
104- case *protocol.MarkedStringWithLanguage:
104+ case *protocol.MarkedStringWithLanguage: //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
105105 if v != nil {
106106 return v.Value
107107 }
@@ -82,7 +82,7 @@ func hoverText(c protocol.HoverContents) string {
82 }82 }
83 case protocol.String:83 case protocol.String:
84 text = string(v)84 text = string(v)
85- case *protocol.MarkedStringWithLanguage:85+ case *protocol.MarkedStringWithLanguage: //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
86 if v != nil {86 if v != nil {
87 text = v.Value87 text = v.Value
88 }88 }
@@ -97,11 +97,11 @@ func hoverText(c protocol.HoverContents) string {
97 }97 }
98 98
99 // markedString is one arm of the deprecated MarkedString union.99 // markedString is one arm of the deprecated MarkedString union.
100-func markedString(m protocol.MarkedString) string {100+func markedString(m protocol.MarkedString) string { //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
101 switch v := m.(type) {101 switch v := m.(type) {
102 case protocol.String:102 case protocol.String:
103 return string(v)103 return string(v)
104- case *protocol.MarkedStringWithLanguage:104+ case *protocol.MarkedStringWithLanguage: //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
105 if v != nil {105 if v != nil {
106 return v.Value106 return v.Value
107 }107 }
modified internal/mcp/client.go +2 -2
@@ -188,7 +188,7 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se
188188 // Perform discovery
189189 tools, err := client.ListTools(ctx)
190190 if err != nil {
191- client.Close()
191+ _ = client.Close()
192192 warnings = append(warnings, fmt.Sprintf("mcp server %s: discovery failed: %v", server.Name, err))
193193 continue
194194 }
@@ -230,6 +230,6 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se
230230 // CloseAll closes all clients.
231231 func CloseAll(clients map[string]*ServerClient) {
232232 for _, sc := range clients {
233- sc.Close()
233+ _ = sc.Close()
234234 }
235235 }
@@ -188,7 +188,7 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se
188 // Perform discovery188 // Perform discovery
189 tools, err := client.ListTools(ctx)189 tools, err := client.ListTools(ctx)
190 if err != nil {190 if err != nil {
191- client.Close()191+ _ = client.Close()
192 warnings = append(warnings, fmt.Sprintf("mcp server %s: discovery failed: %v", server.Name, err))192 warnings = append(warnings, fmt.Sprintf("mcp server %s: discovery failed: %v", server.Name, err))
193 continue193 continue
194 }194 }
@@ -230,6 +230,6 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se
230 // CloseAll closes all clients.230 // CloseAll closes all clients.
231 func CloseAll(clients map[string]*ServerClient) {231 func CloseAll(clients map[string]*ServerClient) {
232 for _, sc := range clients {232 for _, sc := range clients {
233- sc.Close()233+ _ = sc.Close()
234 }234 }
235 }235 }
modified internal/mcp/http_client.go +3 -3
@@ -170,7 +170,7 @@ func (c *HTTPClient) CallTool(ctx context.Context, toolName string, args map[str
170170 func parseCallResult(raw json.RawMessage) (CallToolResult, error) {
171171 var many callToolResponse
172172 if err := json.Unmarshal(raw, &many); err == nil {
173- return CallToolResult{Content: many.Content, IsError: many.IsError}, nil
173+ return CallToolResult(many), nil
174174 }
175175 var single struct {
176176 Content Content `json:"content"`
@@ -252,7 +252,7 @@ func (c *HTTPClient) request(ctx context.Context, method string, params any) (*m
252252 if err != nil {
253253 return nil, err
254254 }
255- defer httpResp.Body.Close()
255+ defer func() { _ = httpResp.Body.Close() }()
256256
257257 resp, err := readResponse(httpResp, id)
258258 if err != nil {
@@ -339,7 +339,7 @@ func (c *HTTPClient) Close() error {
339339 }
340340 _ = c.setHeaders(req)
341341 if resp, err := c.httpClient.Do(req); err == nil {
342- resp.Body.Close()
342+ _ = resp.Body.Close()
343343 }
344344 c.resetSession()
345345 return nil
@@ -170,7 +170,7 @@ func (c *HTTPClient) CallTool(ctx context.Context, toolName string, args map[str
170 func parseCallResult(raw json.RawMessage) (CallToolResult, error) {170 func parseCallResult(raw json.RawMessage) (CallToolResult, error) {
171 var many callToolResponse171 var many callToolResponse
172 if err := json.Unmarshal(raw, &many); err == nil {172 if err := json.Unmarshal(raw, &many); err == nil {
173- return CallToolResult{Content: many.Content, IsError: many.IsError}, nil173+ return CallToolResult(many), nil
174 }174 }
175 var single struct {175 var single struct {
176 Content Content `json:"content"`176 Content Content `json:"content"`
@@ -252,7 +252,7 @@ func (c *HTTPClient) request(ctx context.Context, method string, params any) (*m
252 if err != nil {252 if err != nil {
253 return nil, err253 return nil, err
254 }254 }
255- defer httpResp.Body.Close()255+ defer func() { _ = httpResp.Body.Close() }()
256 256
257 resp, err := readResponse(httpResp, id)257 resp, err := readResponse(httpResp, id)
258 if err != nil {258 if err != nil {
@@ -339,7 +339,7 @@ func (c *HTTPClient) Close() error {
339 }339 }
340 _ = c.setHeaders(req)340 _ = c.setHeaders(req)
341 if resp, err := c.httpClient.Do(req); err == nil {341 if resp, err := c.httpClient.Do(req); err == nil {
342- resp.Body.Close()342+ _ = resp.Body.Close()
343 }343 }
344 c.resetSession()344 c.resetSession()
345 return nil345 return nil
modified internal/mcp/http_client_test.go +4 -4
@@ -97,8 +97,8 @@ func (s *statefulServer) reply(w http.ResponseWriter, id int, result, errObj str
9797 }
9898 w.Header().Set("Content-Type", "text/event-stream")
9999 // A server notification on the same stream must not be taken for the answer.
100- fmt.Fprint(w, "event: message\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\"}}\n\n")
101- fmt.Fprintf(w, "event: message\ndata: %s\n\n", body)
100+ _, _ = fmt.Fprint(w, "event: message\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\"}}\n\n")
101+ _, _ = fmt.Fprintf(w, "event: message\ndata: %s\n\n", body)
102102 }
103103
104104 func newTestClient(t *testing.T, url string) *HTTPClient {
@@ -198,10 +198,10 @@ func TestHTTPClientFallsBackWithoutInitialize(t *testing.T) {
198198 _ = json.NewDecoder(r.Body).Decode(&msg)
199199 w.Header().Set("Content-Type", "application/json")
200200 if msg.Method == "initialize" {
201- fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"error":{"code":-32601,"message":"method not found"}}`, msg.ID)
201+ _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"error":{"code":-32601,"message":"method not found"}}`, msg.ID)
202202 return
203203 }
204- fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"tools":[{"name":"a"}]}}`, msg.ID)
204+ _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"tools":[{"name":"a"}]}}`, msg.ID)
205205 }))
206206 defer ts.Close()
207207
@@ -97,8 +97,8 @@ func (s *statefulServer) reply(w http.ResponseWriter, id int, result, errObj str
97 }97 }
98 w.Header().Set("Content-Type", "text/event-stream")98 w.Header().Set("Content-Type", "text/event-stream")
99 // A server notification on the same stream must not be taken for the answer.99 // A server notification on the same stream must not be taken for the answer.
100- fmt.Fprint(w, "event: message\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\"}}\n\n")100+ _, _ = fmt.Fprint(w, "event: message\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\"}}\n\n")
101- fmt.Fprintf(w, "event: message\ndata: %s\n\n", body)101+ _, _ = fmt.Fprintf(w, "event: message\ndata: %s\n\n", body)
102 }102 }
103 103
104 func newTestClient(t *testing.T, url string) *HTTPClient {104 func newTestClient(t *testing.T, url string) *HTTPClient {
@@ -198,10 +198,10 @@ func TestHTTPClientFallsBackWithoutInitialize(t *testing.T) {
198 _ = json.NewDecoder(r.Body).Decode(&msg)198 _ = json.NewDecoder(r.Body).Decode(&msg)
199 w.Header().Set("Content-Type", "application/json")199 w.Header().Set("Content-Type", "application/json")
200 if msg.Method == "initialize" {200 if msg.Method == "initialize" {
201- fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"error":{"code":-32601,"message":"method not found"}}`, msg.ID)201+ _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"error":{"code":-32601,"message":"method not found"}}`, msg.ID)
202 return202 return
203 }203 }
204- fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"tools":[{"name":"a"}]}}`, msg.ID)204+ _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"tools":[{"name":"a"}]}}`, msg.ID)
205 }))205 }))
206 defer ts.Close()206 defer ts.Close()
207 207
modified internal/mcp/http_wire.go +1 -1
@@ -21,7 +21,7 @@ func checkStatus(resp *http.Response) (*http.Response, error) {
2121 if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
2222 return resp, nil
2323 }
24- defer resp.Body.Close()
24+ defer func() { _ = resp.Body.Close() }()
2525 snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
2626 return nil, &httpStatusError{Status: resp.StatusCode, Body: strings.TrimSpace(string(snippet))}
2727 }
@@ -21,7 +21,7 @@ func checkStatus(resp *http.Response) (*http.Response, error) {
21 if resp.StatusCode >= 200 && resp.StatusCode <= 299 {21 if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
22 return resp, nil22 return resp, nil
23 }23 }
24- defer resp.Body.Close()24+ defer func() { _ = resp.Body.Close() }()
25 snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512))25 snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
26 return nil, &httpStatusError{Status: resp.StatusCode, Body: strings.TrimSpace(string(snippet))}26 return nil, &httpStatusError{Status: resp.StatusCode, Body: strings.TrimSpace(string(snippet))}
27 }27 }
modified internal/mcp/tool_wrapper.go +1 -1
@@ -405,7 +405,7 @@ func callMCPTool(
405405 output.WriteString(content.Text)
406406 } else {
407407 // Non-text content - skip with warning
408- output.WriteString(fmt.Sprintf("[mcp: non-text content of type %s skipped]", content.Type))
408+ fmt.Fprintf(&output, "[mcp: non-text content of type %s skipped]", content.Type)
409409 }
410410 }
411411
@@ -405,7 +405,7 @@ func callMCPTool(
405 output.WriteString(content.Text)405 output.WriteString(content.Text)
406 } else {406 } else {
407 // Non-text content - skip with warning407 // Non-text content - skip with warning
408- output.WriteString(fmt.Sprintf("[mcp: non-text content of type %s skipped]", content.Type))408+ fmt.Fprintf(&output, "[mcp: non-text content of type %s skipped]", content.Type)
409 }409 }
410 }410 }
411 411
modified internal/tools/mcp.go +0 -92
@@ -1,14 +1,9 @@
11 package tools
22
33 import (
4- "context"
5- "fmt"
6-
74 "mm/internal/config"
85 "mm/internal/detector"
96 "mm/internal/mcp"
10- "mm/internal/spinner"
11- "mm/internal/ui"
127
138 "github.com/firebase/genkit/go/ai"
149 "github.com/firebase/genkit/go/genkit"
@@ -35,90 +30,3 @@ func MCP(
3530 // Wrap all tools and get their names
3631 return mcp.WrapToolsWithNames(g, d, clients, defaultPermissions)
3732 }
38-
39-// MCPTool is a pre-configured MCP tool factory for a single server client.
40-// This is useful for testing or when you need to create a tool outside the main MCP setup.
41-func MCPTool(
42- g *genkit.Genkit,
43- d *detector.LoopDetector,
44- client *mcp.ServerClient,
45- permissions string,
46-) (ai.ToolRef, string, error) {
47- if client == nil || len(client.ToolList) == 0 {
48- return nil, "", fmt.Errorf("no tools available from this client")
49- }
50-
51- // For now, just wrap the first tool (simplified)
52- // In a full implementation, this would wrap all tools
53- defaultPermissions := permissions
54- if defaultPermissions == "" {
55- defaultPermissions = config.PermissionsAsk
56- }
57-
58- // This is a simplified version - the full version uses WrapTools
59- // which handles all tools from all clients
60- return nil, "", fmt.Errorf("use MCP() for full tool wrapping")
61-}
62-
63-// MCPEvented is the MCP tool handler for event-consuming front ends.
64-// This is exposed for testing purposes.
65-func MCPEvented(
66- ctx context.Context,
67- s ui.Sink,
68- d *detector.LoopDetector,
69- client *mcp.ServerClient,
70- mcpTool mcp.Tool,
71- toolName string,
72- in map[string]any,
73- permissions string,
74-) (string, error) {
75- // Delegate to the mcp package's internal implementation
76- // This is a pass-through for testing
77- return "", fmt.Errorf("use MCP() for full implementation")
78-}
79-
80-// MCPTerminal is the MCP tool handler for terminal mode.
81-// This is exposed for testing purposes.
82-func MCPTerminal(
83- ctx context.Context,
84- d *detector.LoopDetector,
85- client *mcp.ServerClient,
86- mcpTool mcp.Tool,
87- toolName string,
88- in map[string]any,
89-) (string, error) {
90- // Delegate to the mcp package's internal implementation
91- // This is a pass-through for testing
92- return "", fmt.Errorf("use MCP() for full implementation")
93-}
94-
95-// EchoMCPOutput displays MCP tool output in the terminal.
96-// This is separate from echoOutput to allow for different formatting.
97-func EchoMCPOutput(out string) {
98- if config.Cfg.PreviewLines <= 0 {
99- return
100- }
101-
102- // For now, just use the standard echoOutput
103- // Could be customized for MCP-specific formatting
104- echoOutput(out)
105-}
106-
107-// announceMCP prints the MCP tool call announcement in the terminal.
108-func announceMCP(toolName, description string) {
109- spinner.Suspend(func() {
110- printMu.Lock()
111- fmt.Printf("\n🔧 %s: %s\n", toolName, truncateDesc(description))
112- printMu.Unlock()
113- })
114-}
115-
116-// truncateDesc truncates a description to a reasonable length for display.
117-func truncateDesc(desc string) string {
118- const maxDescLen = 60
119- r := []rune(desc)
120- if len(r) <= maxDescLen {
121- return desc
122- }
123- return string(r[:maxDescLen-3]) + "..."
124-}
@@ -1,14 +1,9 @@
1 package tools1 package tools
2 2
3 import (3 import (
4- "context"
5- "fmt"
6-
7 "mm/internal/config"4 "mm/internal/config"
8 "mm/internal/detector"5 "mm/internal/detector"
9 "mm/internal/mcp"6 "mm/internal/mcp"
10- "mm/internal/spinner"
11- "mm/internal/ui"
12 7
13 "github.com/firebase/genkit/go/ai"8 "github.com/firebase/genkit/go/ai"
14 "github.com/firebase/genkit/go/genkit"9 "github.com/firebase/genkit/go/genkit"
@@ -35,90 +30,3 @@ func MCP(
35 // Wrap all tools and get their names30 // Wrap all tools and get their names
36 return mcp.WrapToolsWithNames(g, d, clients, defaultPermissions)31 return mcp.WrapToolsWithNames(g, d, clients, defaultPermissions)
37 }32 }
38-
39-// MCPTool is a pre-configured MCP tool factory for a single server client.
40-// This is useful for testing or when you need to create a tool outside the main MCP setup.
41-func MCPTool(
42- g *genkit.Genkit,
43- d *detector.LoopDetector,
44- client *mcp.ServerClient,
45- permissions string,
46-) (ai.ToolRef, string, error) {
47- if client == nil || len(client.ToolList) == 0 {
48- return nil, "", fmt.Errorf("no tools available from this client")
49- }
50-
51- // For now, just wrap the first tool (simplified)
52- // In a full implementation, this would wrap all tools
53- defaultPermissions := permissions
54- if defaultPermissions == "" {
55- defaultPermissions = config.PermissionsAsk
56- }
57-
58- // This is a simplified version - the full version uses WrapTools
59- // which handles all tools from all clients
60- return nil, "", fmt.Errorf("use MCP() for full tool wrapping")
61-}
62-
63-// MCPEvented is the MCP tool handler for event-consuming front ends.
64-// This is exposed for testing purposes.
65-func MCPEvented(
66- ctx context.Context,
67- s ui.Sink,
68- d *detector.LoopDetector,
69- client *mcp.ServerClient,
70- mcpTool mcp.Tool,
71- toolName string,
72- in map[string]any,
73- permissions string,
74-) (string, error) {
75- // Delegate to the mcp package's internal implementation
76- // This is a pass-through for testing
77- return "", fmt.Errorf("use MCP() for full implementation")
78-}
79-
80-// MCPTerminal is the MCP tool handler for terminal mode.
81-// This is exposed for testing purposes.
82-func MCPTerminal(
83- ctx context.Context,
84- d *detector.LoopDetector,
85- client *mcp.ServerClient,
86- mcpTool mcp.Tool,
87- toolName string,
88- in map[string]any,
89-) (string, error) {
90- // Delegate to the mcp package's internal implementation
91- // This is a pass-through for testing
92- return "", fmt.Errorf("use MCP() for full implementation")
93-}
94-
95-// EchoMCPOutput displays MCP tool output in the terminal.
96-// This is separate from echoOutput to allow for different formatting.
97-func EchoMCPOutput(out string) {
98- if config.Cfg.PreviewLines <= 0 {
99- return
100- }
101-
102- // For now, just use the standard echoOutput
103- // Could be customized for MCP-specific formatting
104- echoOutput(out)
105-}
106-
107-// announceMCP prints the MCP tool call announcement in the terminal.
108-func announceMCP(toolName, description string) {
109- spinner.Suspend(func() {
110- printMu.Lock()
111- fmt.Printf("\n🔧 %s: %s\n", toolName, truncateDesc(description))
112- printMu.Unlock()
113- })
114-}
115-
116-// truncateDesc truncates a description to a reasonable length for display.
117-func truncateDesc(desc string) string {
118- const maxDescLen = 60
119- r := []rune(desc)
120- if len(r) <= maxDescLen {
121- return desc
122- }
123- return string(r[:maxDescLen-3]) + "..."
124-}
modified internal/tools/plan.go +18 -213
@@ -351,33 +351,14 @@ func (s *PlanStore) GetStatus(name string) (string, string, error) {
351351 if plan.Current == 0 {
352352 // Not started, show Task 1
353353 task := plan.Tasks[0]
354- statusIcon := "⏳"
355- if task.Status == "done" {
356- statusIcon = "✅"
357- } else if task.Status == "in_progress" {
358- statusIcon = "🔄"
359- } else if task.Status == "failed" {
360- statusIcon = "❌"
361- }
362- return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon, 1, task.Description, task.Status),
354+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon(task.Status), 1, task.Description, task.Status),
363355 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
364356 }
365357
366358 // Current > 0, show Task Current
367359 if plan.Current <= len(plan.Tasks) {
368360 task := plan.Tasks[plan.Current-1]
369- statusIcon := ""
370- switch task.Status {
371- case "done":
372- statusIcon = "✅"
373- case "in_progress":
374- statusIcon = "🔄"
375- case "failed":
376- statusIcon = "❌"
377- default:
378- statusIcon = "⏳"
379- }
380- return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon, plan.Current, task.Description, task.Status),
361+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon(task.Status), plan.Current, task.Description, task.Status),
381362 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
382363 }
383364
@@ -386,6 +367,20 @@ func (s *PlanStore) GetStatus(name string) (string, string, error) {
386367 fmt.Sprintf("Plan '%s': All %d tasks done ✅", name, len(plan.Tasks)), nil
387368 }
388369
370+// statusIcon is the display mark for a task status.
371+func statusIcon(status string) string {
372+ switch status {
373+ case "done":
374+ return "✅"
375+ case "in_progress":
376+ return "🔄"
377+ case "failed":
378+ return "❌"
379+ default:
380+ return "⏳"
381+ }
382+}
383+
389384 // ClearPlans removes all plans for the current session.
390385 // Called when /new command is issued.
391386 func (s *PlanStore) ClearPlans() {
@@ -402,15 +397,6 @@ func (s *PlanStore) ClearAllPlans() {
402397 s.plans = make(map[string]map[string]*Plan)
403398 }
404399
405-// announcePlan prints the display line for a plan operation.
406-func announcePlan(line string) {
407- spinner.Suspend(func() {
408- printMu.Lock()
409- fmt.Printf("\n📋 %s\n", clip(line))
410- printMu.Unlock()
411- })
412-}
413-
414400 // PlanTool declares the `plan` tool.
415401 func PlanTool(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef {
416402 description := `Create and manage multi-step plans. When the user gives you a list of tasks to do (e.g., "Do: 1. X, 2. Y, 3. Z"), use this tool to create a plan and track progress.
@@ -457,101 +443,7 @@ Always show the user the current plan status using the status action.`
457443 }
458444
459445 spinner.Start("Running")
460-
461- var result string
462-
463- switch in.Action {
464- case "create":
465- plan, msg, createErr := planStore.CreatePlan(in.Name, in.Tasks)
466- if createErr != nil {
467- result = fmt.Sprintf("[error: %v]", createErr)
468- break
469- }
470- // Return plan info as JSON for model, plus text summary
471- output := PlanOutput{
472- Result: msg,
473- PlanName: plan.Name,
474- Plan: plan,
475- Current: plan.Current,
476- Total: len(plan.Tasks),
477- }
478- outputBytes, _ := json.Marshal(output)
479- result = string(outputBytes)
480-
481- case "get":
482- plan, msg, getErr := planStore.GetPlan(in.Name)
483- if getErr != nil {
484- result = fmt.Sprintf("[error: %v]", getErr)
485- break
486- }
487- output := PlanOutput{
488- Result: msg,
489- Plan: plan,
490- }
491- outputBytes, _ := json.Marshal(output)
492- result = string(outputBytes)
493-
494- case "update":
495- _, msg, updateErr := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
496- if updateErr != nil {
497- result = fmt.Sprintf("[error: %v]", updateErr)
498- break
499- }
500- // Get status line
501- statusLine, _, _ := planStore.GetStatus(in.Name)
502- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
503-
504- case "next":
505- _, nextTask, msg, nextErr := planStore.NextTask(in.Name)
506- if nextErr != nil {
507- result = fmt.Sprintf("[error: %v]", nextErr)
508- break
509- }
510- // Get status line
511- statusLine, _, _ := planStore.GetStatus(in.Name)
512- if nextTask != nil {
513- result = fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
514- msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
515- } else {
516- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
517- }
518-
519- case "list":
520- plans, msg, listErr := planStore.ListPlans()
521- if listErr != nil {
522- result = fmt.Sprintf("[error: %v]", listErr)
523- break
524- }
525- if len(plans) == 0 {
526- result = `{"result":"` + msg + `","plans":[]}`
527- break
528- }
529- // Build simple list
530- var planNames []string
531- for _, p := range plans {
532- planNames = append(planNames, p.Name)
533- }
534- result = fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(planNames, `", "`))
535-
536- case "delete":
537- msg, delErr := planStore.DeletePlan(in.Name)
538- if delErr != nil {
539- result = fmt.Sprintf("[error: %v]", delErr)
540- break
541- }
542- result = fmt.Sprintf(`{"result":"%s"}`, msg)
543-
544- case "status":
545- statusLine, msg, statusErr := planStore.GetStatus(in.Name)
546- if statusErr != nil {
547- result = fmt.Sprintf("[error: %v]", statusErr)
548- break
549- }
550- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
551-
552- default:
553- result = fmt.Sprintf("[error: unknown action '%s'. Valid actions: create, get, update, next, list, delete, status]", in.Action)
554- }
446+ result := runPlanAction(in, true)
555447
556448 // Record in loop detector
557449 inputJSON, _ := json.Marshal(in)
@@ -594,94 +486,7 @@ func planEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in pl
594486
595487 s.ToolRunning(ev.ID)
596488
597- // Execute the action
598- var result string
599-
600- switch in.Action {
601- case "create":
602- plan, msg, createErr := planStore.CreatePlan(in.Name, in.Tasks)
603- if createErr != nil {
604- result = fmt.Sprintf("[error: %v]", createErr)
605- } else {
606- output := PlanOutput{
607- Result: msg,
608- PlanName: plan.Name,
609- Current: plan.Current,
610- Total: len(plan.Tasks),
611- }
612- outputBytes, _ := json.Marshal(output)
613- result = string(outputBytes)
614- }
615-
616- case "get":
617- plan, msg, getErr := planStore.GetPlan(in.Name)
618- if getErr != nil {
619- result = fmt.Sprintf("[error: %v]", getErr)
620- } else {
621- output := PlanOutput{
622- Result: msg,
623- Plan: plan,
624- }
625- outputBytes, _ := json.Marshal(output)
626- result = string(outputBytes)
627- }
628-
629- case "update":
630- _, msg, updateErr := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
631- if updateErr != nil {
632- result = fmt.Sprintf("[error: %v]", updateErr)
633- } else {
634- statusLine, _, _ := planStore.GetStatus(in.Name)
635- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
636- }
637-
638- case "next":
639- _, nextTask, msg, nextErr := planStore.NextTask(in.Name)
640- if nextErr != nil {
641- result = fmt.Sprintf("[error: %v]", nextErr)
642- } else {
643- statusLine, _, _ := planStore.GetStatus(in.Name)
644- if nextTask != nil {
645- result = fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
646- msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
647- } else {
648- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
649- }
650- }
651-
652- case "list":
653- plans, msg, listErr := planStore.ListPlans()
654- if listErr != nil {
655- result = fmt.Sprintf("[error: %v]", listErr)
656- } else if len(plans) == 0 {
657- result = `{"result":"` + msg + `","plans":[]}`
658- } else {
659- var planNames []string
660- for _, p := range plans {
661- planNames = append(planNames, p.Name)
662- }
663- result = fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(planNames, `", "`))
664- }
665-
666- case "delete":
667- msg, delErr := planStore.DeletePlan(in.Name)
668- if delErr != nil {
669- result = fmt.Sprintf("[error: %v]", delErr)
670- } else {
671- result = fmt.Sprintf(`{"result":"%s"}`, msg)
672- }
673-
674- case "status":
675- statusLine, msg, statusErr := planStore.GetStatus(in.Name)
676- if statusErr != nil {
677- result = fmt.Sprintf("[error: %v]", statusErr)
678- } else {
679- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
680- }
681-
682- default:
683- result = fmt.Sprintf("[error: unknown action '%s']", in.Action)
684- }
489+ result := runPlanAction(in, false)
685490
686491 // Check if failed
687492 failed := strings.HasPrefix(result, "[error:")
@@ -351,33 +351,14 @@ func (s *PlanStore) GetStatus(name string) (string, string, error) {
351 if plan.Current == 0 {351 if plan.Current == 0 {
352 // Not started, show Task 1352 // Not started, show Task 1
353 task := plan.Tasks[0]353 task := plan.Tasks[0]
354- statusIcon := "⏳"354+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon(task.Status), 1, task.Description, task.Status),
355- if task.Status == "done" {
356- statusIcon = "✅"
357- } else if task.Status == "in_progress" {
358- statusIcon = "🔄"
359- } else if task.Status == "failed" {
360- statusIcon = "❌"
361- }
362- return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon, 1, task.Description, task.Status),
363 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil355 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
364 }356 }
365 357
366 // Current > 0, show Task Current358 // Current > 0, show Task Current
367 if plan.Current <= len(plan.Tasks) {359 if plan.Current <= len(plan.Tasks) {
368 task := plan.Tasks[plan.Current-1]360 task := plan.Tasks[plan.Current-1]
369- statusIcon := ""361+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon(task.Status), plan.Current, task.Description, task.Status),
370- switch task.Status {
371- case "done":
372- statusIcon = "✅"
373- case "in_progress":
374- statusIcon = "🔄"
375- case "failed":
376- statusIcon = "❌"
377- default:
378- statusIcon = "⏳"
379- }
380- return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon, plan.Current, task.Description, task.Status),
381 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil362 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
382 }363 }
383 364
@@ -386,6 +367,20 @@ func (s *PlanStore) GetStatus(name string) (string, string, error) {
386 fmt.Sprintf("Plan '%s': All %d tasks done ✅", name, len(plan.Tasks)), nil367 fmt.Sprintf("Plan '%s': All %d tasks done ✅", name, len(plan.Tasks)), nil
387 }368 }
388 369
370+// statusIcon is the display mark for a task status.
371+func statusIcon(status string) string {
372+ switch status {
373+ case "done":
374+ return "✅"
375+ case "in_progress":
376+ return "🔄"
377+ case "failed":
378+ return "❌"
379+ default:
380+ return "⏳"
381+ }
382+}
383+
389 // ClearPlans removes all plans for the current session.384 // ClearPlans removes all plans for the current session.
390 // Called when /new command is issued.385 // Called when /new command is issued.
391 func (s *PlanStore) ClearPlans() {386 func (s *PlanStore) ClearPlans() {
@@ -402,15 +397,6 @@ func (s *PlanStore) ClearAllPlans() {
402 s.plans = make(map[string]map[string]*Plan)397 s.plans = make(map[string]map[string]*Plan)
403 }398 }
404 399
405-// announcePlan prints the display line for a plan operation.
406-func announcePlan(line string) {
407- spinner.Suspend(func() {
408- printMu.Lock()
409- fmt.Printf("\n📋 %s\n", clip(line))
410- printMu.Unlock()
411- })
412-}
413-
414 // PlanTool declares the `plan` tool.400 // PlanTool declares the `plan` tool.
415 func PlanTool(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef {401 func PlanTool(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef {
416 description := `Create and manage multi-step plans. When the user gives you a list of tasks to do (e.g., "Do: 1. X, 2. Y, 3. Z"), use this tool to create a plan and track progress.402 description := `Create and manage multi-step plans. When the user gives you a list of tasks to do (e.g., "Do: 1. X, 2. Y, 3. Z"), use this tool to create a plan and track progress.
@@ -457,101 +443,7 @@ Always show the user the current plan status using the status action.`
457 }443 }
458 444
459 spinner.Start("Running")445 spinner.Start("Running")
460-446+ result := runPlanAction(in, true)
461- var result string
462-
463- switch in.Action {
464- case "create":
465- plan, msg, createErr := planStore.CreatePlan(in.Name, in.Tasks)
466- if createErr != nil {
467- result = fmt.Sprintf("[error: %v]", createErr)
468- break
469- }
470- // Return plan info as JSON for model, plus text summary
471- output := PlanOutput{
472- Result: msg,
473- PlanName: plan.Name,
474- Plan: plan,
475- Current: plan.Current,
476- Total: len(plan.Tasks),
477- }
478- outputBytes, _ := json.Marshal(output)
479- result = string(outputBytes)
480-
481- case "get":
482- plan, msg, getErr := planStore.GetPlan(in.Name)
483- if getErr != nil {
484- result = fmt.Sprintf("[error: %v]", getErr)
485- break
486- }
487- output := PlanOutput{
488- Result: msg,
489- Plan: plan,
490- }
491- outputBytes, _ := json.Marshal(output)
492- result = string(outputBytes)
493-
494- case "update":
495- _, msg, updateErr := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
496- if updateErr != nil {
497- result = fmt.Sprintf("[error: %v]", updateErr)
498- break
499- }
500- // Get status line
501- statusLine, _, _ := planStore.GetStatus(in.Name)
502- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
503-
504- case "next":
505- _, nextTask, msg, nextErr := planStore.NextTask(in.Name)
506- if nextErr != nil {
507- result = fmt.Sprintf("[error: %v]", nextErr)
508- break
509- }
510- // Get status line
511- statusLine, _, _ := planStore.GetStatus(in.Name)
512- if nextTask != nil {
513- result = fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
514- msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
515- } else {
516- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
517- }
518-
519- case "list":
520- plans, msg, listErr := planStore.ListPlans()
521- if listErr != nil {
522- result = fmt.Sprintf("[error: %v]", listErr)
523- break
524- }
525- if len(plans) == 0 {
526- result = `{"result":"` + msg + `","plans":[]}`
527- break
528- }
529- // Build simple list
530- var planNames []string
531- for _, p := range plans {
532- planNames = append(planNames, p.Name)
533- }
534- result = fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(planNames, `", "`))
535-
536- case "delete":
537- msg, delErr := planStore.DeletePlan(in.Name)
538- if delErr != nil {
539- result = fmt.Sprintf("[error: %v]", delErr)
540- break
541- }
542- result = fmt.Sprintf(`{"result":"%s"}`, msg)
543-
544- case "status":
545- statusLine, msg, statusErr := planStore.GetStatus(in.Name)
546- if statusErr != nil {
547- result = fmt.Sprintf("[error: %v]", statusErr)
548- break
549- }
550- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
551-
552- default:
553- result = fmt.Sprintf("[error: unknown action '%s'. Valid actions: create, get, update, next, list, delete, status]", in.Action)
554- }
555 447
556 // Record in loop detector448 // Record in loop detector
557 inputJSON, _ := json.Marshal(in)449 inputJSON, _ := json.Marshal(in)
@@ -594,94 +486,7 @@ func planEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in pl
594 486
595 s.ToolRunning(ev.ID)487 s.ToolRunning(ev.ID)
596 488
597- // Execute the action489+ result := runPlanAction(in, false)
598- var result string
599-
600- switch in.Action {
601- case "create":
602- plan, msg, createErr := planStore.CreatePlan(in.Name, in.Tasks)
603- if createErr != nil {
604- result = fmt.Sprintf("[error: %v]", createErr)
605- } else {
606- output := PlanOutput{
607- Result: msg,
608- PlanName: plan.Name,
609- Current: plan.Current,
610- Total: len(plan.Tasks),
611- }
612- outputBytes, _ := json.Marshal(output)
613- result = string(outputBytes)
614- }
615-
616- case "get":
617- plan, msg, getErr := planStore.GetPlan(in.Name)
618- if getErr != nil {
619- result = fmt.Sprintf("[error: %v]", getErr)
620- } else {
621- output := PlanOutput{
622- Result: msg,
623- Plan: plan,
624- }
625- outputBytes, _ := json.Marshal(output)
626- result = string(outputBytes)
627- }
628-
629- case "update":
630- _, msg, updateErr := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
631- if updateErr != nil {
632- result = fmt.Sprintf("[error: %v]", updateErr)
633- } else {
634- statusLine, _, _ := planStore.GetStatus(in.Name)
635- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
636- }
637-
638- case "next":
639- _, nextTask, msg, nextErr := planStore.NextTask(in.Name)
640- if nextErr != nil {
641- result = fmt.Sprintf("[error: %v]", nextErr)
642- } else {
643- statusLine, _, _ := planStore.GetStatus(in.Name)
644- if nextTask != nil {
645- result = fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
646- msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
647- } else {
648- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
649- }
650- }
651-
652- case "list":
653- plans, msg, listErr := planStore.ListPlans()
654- if listErr != nil {
655- result = fmt.Sprintf("[error: %v]", listErr)
656- } else if len(plans) == 0 {
657- result = `{"result":"` + msg + `","plans":[]}`
658- } else {
659- var planNames []string
660- for _, p := range plans {
661- planNames = append(planNames, p.Name)
662- }
663- result = fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(planNames, `", "`))
664- }
665-
666- case "delete":
667- msg, delErr := planStore.DeletePlan(in.Name)
668- if delErr != nil {
669- result = fmt.Sprintf("[error: %v]", delErr)
670- } else {
671- result = fmt.Sprintf(`{"result":"%s"}`, msg)
672- }
673-
674- case "status":
675- statusLine, msg, statusErr := planStore.GetStatus(in.Name)
676- if statusErr != nil {
677- result = fmt.Sprintf("[error: %v]", statusErr)
678- } else {
679- result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
680- }
681-
682- default:
683- result = fmt.Sprintf("[error: unknown action '%s']", in.Action)
684- }
685 490
686 // Check if failed491 // Check if failed
687 failed := strings.HasPrefix(result, "[error:")492 failed := strings.HasPrefix(result, "[error:")
added internal/tools/plan_actions.go +115 -0
new file mode 100644
@@ -0,0 +1,115 @@
1+package tools
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "strings"
7+)
8+
9+// planActions runs one plan action and renders its result for the model.
10+// "create" is not here: it depends on the front end (see runPlanAction).
11+var planActions = map[string]func(planInput) string{
12+ "get": planGet,
13+ "update": planUpdate,
14+ "next": planNext,
15+ "list": planList,
16+ "delete": planDelete,
17+ "status": planStatus,
18+}
19+
20+// runPlanAction is the one dispatch both front ends share. withPlan puts the
21+// whole created plan in the "create" answer — the terminal does, the
22+// event-consuming front ends (ACP) only give its name and position.
23+func runPlanAction(in planInput, withPlan bool) string {
24+ if in.Action == "create" {
25+ return planCreate(in, withPlan)
26+ }
27+ if run, ok := planActions[in.Action]; ok {
28+ return run(in)
29+ }
30+ return fmt.Sprintf("[error: unknown action '%s'. Valid actions: create, get, update, next, list, delete, status]", in.Action)
31+}
32+
33+// planError is the error answer every action gives.
34+func planError(err error) string {
35+ return fmt.Sprintf("[error: %v]", err)
36+}
37+
38+// planJSON renders a PlanOutput.
39+func planJSON(out PlanOutput) string {
40+ b, _ := json.Marshal(out)
41+ return string(b)
42+}
43+
44+func planCreate(in planInput, withPlan bool) string {
45+ plan, msg, err := planStore.CreatePlan(in.Name, in.Tasks)
46+ if err != nil {
47+ return planError(err)
48+ }
49+ out := PlanOutput{Result: msg, PlanName: plan.Name, Current: plan.Current, Total: len(plan.Tasks)}
50+ if withPlan {
51+ out.Plan = plan
52+ }
53+ return planJSON(out)
54+}
55+
56+func planGet(in planInput) string {
57+ plan, msg, err := planStore.GetPlan(in.Name)
58+ if err != nil {
59+ return planError(err)
60+ }
61+ return planJSON(PlanOutput{Result: msg, Plan: plan})
62+}
63+
64+func planUpdate(in planInput) string {
65+ _, msg, err := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
66+ if err != nil {
67+ return planError(err)
68+ }
69+ statusLine, _, _ := planStore.GetStatus(in.Name)
70+ return fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
71+}
72+
73+func planNext(in planInput) string {
74+ _, nextTask, msg, err := planStore.NextTask(in.Name)
75+ if err != nil {
76+ return planError(err)
77+ }
78+ statusLine, _, _ := planStore.GetStatus(in.Name)
79+ if nextTask == nil {
80+ return fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
81+ }
82+ return fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
83+ msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
84+}
85+
86+func planList(planInput) string {
87+ plans, msg, err := planStore.ListPlans()
88+ if err != nil {
89+ return planError(err)
90+ }
91+ if len(plans) == 0 {
92+ return `{"result":"` + msg + `","plans":[]}`
93+ }
94+ names := make([]string, 0, len(plans))
95+ for _, p := range plans {
96+ names = append(names, p.Name)
97+ }
98+ return fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(names, `", "`))
99+}
100+
101+func planDelete(in planInput) string {
102+ msg, err := planStore.DeletePlan(in.Name)
103+ if err != nil {
104+ return planError(err)
105+ }
106+ return fmt.Sprintf(`{"result":"%s"}`, msg)
107+}
108+
109+func planStatus(in planInput) string {
110+ statusLine, msg, err := planStore.GetStatus(in.Name)
111+ if err != nil {
112+ return planError(err)
113+ }
114+ return fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
115+}
new file mode 100644
@@ -0,0 +1,115 @@
1+package tools
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "strings"
7+)
8+
9+// planActions runs one plan action and renders its result for the model.
10+// "create" is not here: it depends on the front end (see runPlanAction).
11+var planActions = map[string]func(planInput) string{
12+ "get": planGet,
13+ "update": planUpdate,
14+ "next": planNext,
15+ "list": planList,
16+ "delete": planDelete,
17+ "status": planStatus,
18+}
19+
20+// runPlanAction is the one dispatch both front ends share. withPlan puts the
21+// whole created plan in the "create" answer — the terminal does, the
22+// event-consuming front ends (ACP) only give its name and position.
23+func runPlanAction(in planInput, withPlan bool) string {
24+ if in.Action == "create" {
25+ return planCreate(in, withPlan)
26+ }
27+ if run, ok := planActions[in.Action]; ok {
28+ return run(in)
29+ }
30+ return fmt.Sprintf("[error: unknown action '%s'. Valid actions: create, get, update, next, list, delete, status]", in.Action)
31+}
32+
33+// planError is the error answer every action gives.
34+func planError(err error) string {
35+ return fmt.Sprintf("[error: %v]", err)
36+}
37+
38+// planJSON renders a PlanOutput.
39+func planJSON(out PlanOutput) string {
40+ b, _ := json.Marshal(out)
41+ return string(b)
42+}
43+
44+func planCreate(in planInput, withPlan bool) string {
45+ plan, msg, err := planStore.CreatePlan(in.Name, in.Tasks)
46+ if err != nil {
47+ return planError(err)
48+ }
49+ out := PlanOutput{Result: msg, PlanName: plan.Name, Current: plan.Current, Total: len(plan.Tasks)}
50+ if withPlan {
51+ out.Plan = plan
52+ }
53+ return planJSON(out)
54+}
55+
56+func planGet(in planInput) string {
57+ plan, msg, err := planStore.GetPlan(in.Name)
58+ if err != nil {
59+ return planError(err)
60+ }
61+ return planJSON(PlanOutput{Result: msg, Plan: plan})
62+}
63+
64+func planUpdate(in planInput) string {
65+ _, msg, err := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
66+ if err != nil {
67+ return planError(err)
68+ }
69+ statusLine, _, _ := planStore.GetStatus(in.Name)
70+ return fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
71+}
72+
73+func planNext(in planInput) string {
74+ _, nextTask, msg, err := planStore.NextTask(in.Name)
75+ if err != nil {
76+ return planError(err)
77+ }
78+ statusLine, _, _ := planStore.GetStatus(in.Name)
79+ if nextTask == nil {
80+ return fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
81+ }
82+ return fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
83+ msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
84+}
85+
86+func planList(planInput) string {
87+ plans, msg, err := planStore.ListPlans()
88+ if err != nil {
89+ return planError(err)
90+ }
91+ if len(plans) == 0 {
92+ return `{"result":"` + msg + `","plans":[]}`
93+ }
94+ names := make([]string, 0, len(plans))
95+ for _, p := range plans {
96+ names = append(names, p.Name)
97+ }
98+ return fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(names, `", "`))
99+}
100+
101+func planDelete(in planInput) string {
102+ msg, err := planStore.DeletePlan(in.Name)
103+ if err != nil {
104+ return planError(err)
105+ }
106+ return fmt.Sprintf(`{"result":"%s"}`, msg)
107+}
108+
109+func planStatus(in planInput) string {
110+ statusLine, msg, err := planStore.GetStatus(in.Name)
111+ if err != nil {
112+ return planError(err)
113+ }
114+ return fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
115+}
added internal/tools/plan_actions_test.go +43 -0
new file mode 100644
@@ -0,0 +1,43 @@
1+package tools
2+
3+import (
4+ "encoding/json"
5+ "strings"
6+ "testing"
7+)
8+
9+// runPlanAction answers every action the two front ends used to dispatch
10+// separately: the terminal's "create" carries the whole plan, ACP's does not.
11+func TestRunPlanAction(t *testing.T) {
12+ ClearAllPlansForTesting()
13+ tasks := []PlanTaskInput{{Description: `say "hi"`}, {Description: "B"}}
14+
15+ var full, short PlanOutput
16+ if err := json.Unmarshal([]byte(runPlanAction(planInput{Action: "create", Name: "p1", Tasks: tasks}, true)), &full); err != nil || full.Plan == nil {
17+ t.Fatalf("create with plan: %+v, %v", full, err)
18+ }
19+ if err := json.Unmarshal([]byte(runPlanAction(planInput{Action: "create", Name: "p2", Tasks: tasks}, false)), &short); err != nil {
20+ t.Fatalf("create without plan: %v", err)
21+ }
22+ if short.Plan != nil || short.PlanName != "p2" || short.Total != 2 {
23+ t.Errorf("create without plan: %+v", short)
24+ }
25+
26+ for _, tc := range []struct {
27+ in planInput
28+ want string
29+ }{
30+ {planInput{Action: "get", Name: "p1"}, `"plan":`},
31+ {planInput{Action: "update", Name: "p1", TaskID: 1, Status: "in_progress"}, `"status":"[1/2]`},
32+ {planInput{Action: "next", Name: "p1"}, `"nextTask":{"id":1,"description":"say \"hi\""}`},
33+ {planInput{Action: "status", Name: "p1"}, `"status":"[1/2]`},
34+ {planInput{Action: "list"}, `"plans":["`},
35+ {planInput{Action: "delete", Name: "p2"}, `{"result":"`},
36+ {planInput{Action: "get", Name: "p2"}, "[error: "},
37+ {planInput{Action: "explode"}, "[error: unknown action 'explode'. Valid actions:"},
38+ } {
39+ if got := runPlanAction(tc.in, true); !strings.Contains(got, tc.want) {
40+ t.Errorf("%s %s: %s, want it to contain %s", tc.in.Action, tc.in.Name, got, tc.want)
41+ }
42+ }
43+}
new file mode 100644
@@ -0,0 +1,43 @@
1+package tools
2+
3+import (
4+ "encoding/json"
5+ "strings"
6+ "testing"
7+)
8+
9+// runPlanAction answers every action the two front ends used to dispatch
10+// separately: the terminal's "create" carries the whole plan, ACP's does not.
11+func TestRunPlanAction(t *testing.T) {
12+ ClearAllPlansForTesting()
13+ tasks := []PlanTaskInput{{Description: `say "hi"`}, {Description: "B"}}
14+
15+ var full, short PlanOutput
16+ if err := json.Unmarshal([]byte(runPlanAction(planInput{Action: "create", Name: "p1", Tasks: tasks}, true)), &full); err != nil || full.Plan == nil {
17+ t.Fatalf("create with plan: %+v, %v", full, err)
18+ }
19+ if err := json.Unmarshal([]byte(runPlanAction(planInput{Action: "create", Name: "p2", Tasks: tasks}, false)), &short); err != nil {
20+ t.Fatalf("create without plan: %v", err)
21+ }
22+ if short.Plan != nil || short.PlanName != "p2" || short.Total != 2 {
23+ t.Errorf("create without plan: %+v", short)
24+ }
25+
26+ for _, tc := range []struct {
27+ in planInput
28+ want string
29+ }{
30+ {planInput{Action: "get", Name: "p1"}, `"plan":`},
31+ {planInput{Action: "update", Name: "p1", TaskID: 1, Status: "in_progress"}, `"status":"[1/2]`},
32+ {planInput{Action: "next", Name: "p1"}, `"nextTask":{"id":1,"description":"say \"hi\""}`},
33+ {planInput{Action: "status", Name: "p1"}, `"status":"[1/2]`},
34+ {planInput{Action: "list"}, `"plans":["`},
35+ {planInput{Action: "delete", Name: "p2"}, `{"result":"`},
36+ {planInput{Action: "get", Name: "p2"}, "[error: "},
37+ {planInput{Action: "explode"}, "[error: unknown action 'explode'. Valid actions:"},
38+ } {
39+ if got := runPlanAction(tc.in, true); !strings.Contains(got, tc.want) {
40+ t.Errorf("%s %s: %s, want it to contain %s", tc.in.Action, tc.in.Name, got, tc.want)
41+ }
42+ }
43+}
modified internal/tools/plan_test.go +21 -12
@@ -227,7 +227,7 @@ func TestPlanStore_NextTask(t *testing.T) {
227227 }
228228
229229 // Second next - should mark task 1 as done, go to task 2
230- plan, nextTask, msg, err = store.NextTask("next-test")
230+ plan, nextTask, _, err = store.NextTask("next-test")
231231 if err != nil {
232232 t.Fatalf("Second NextTask failed: %v", err)
233233 }
@@ -242,7 +242,7 @@ func TestPlanStore_NextTask(t *testing.T) {
242242 }
243243
244244 // Third next - should mark task 2 as done, go to task 3
245- plan, nextTask, msg, err = store.NextTask("next-test")
245+ plan, nextTask, _, err = store.NextTask("next-test")
246246 if err != nil {
247247 t.Fatalf("Third NextTask failed: %v", err)
248248 }
@@ -362,9 +362,12 @@ func TestPlanStore_GetStatus(t *testing.T) {
362362 }
363363
364364 // Complete all tasks (need 3 more NextTask calls to complete all 3 tasks)
365- store.NextTask("status-test") // Marks Task 1 as done, moves to Task 2
366- store.NextTask("status-test") // Marks Task 2 as done, moves to Task 3
367- store.NextTask("status-test") // Marks Task 3 as done, moves to Current=4 (complete)
365+ // Each call marks the current task done and moves on; the third reaches Current=4 (complete).
366+ for i := 0; i < 3; i++ {
367+ if _, _, _, err := store.NextTask("status-test"); err != nil {
368+ t.Fatalf("NextTask %d failed: %v", i+1, err)
369+ }
370+ }
368371
369372 // Get status when complete
370373 statusLine, _, err = store.GetStatus("status-test")
@@ -396,9 +399,7 @@ func TestPlanStore_ListPlans(t *testing.T) {
396399 }
397400
398401 // Create multiple plans
399- store.CreatePlan("plan-a", []PlanTaskInput{{Description: "A1"}})
400- store.CreatePlan("plan-b", []PlanTaskInput{{Description: "B1"}})
401- store.CreatePlan("plan-c", []PlanTaskInput{{Description: "C1"}})
402+ mustCreatePlans(t, store, "plan-a", "plan-b", "plan-c")
402403
403404 // List plans
404405 plans, msg, err = store.ListPlans()
@@ -419,8 +420,7 @@ func TestPlanStore_ClearPlans(t *testing.T) {
419420 store := planStore
420421
421422 // Create some plans
422- store.CreatePlan("clear-test-1", []PlanTaskInput{{Description: "T1"}})
423- store.CreatePlan("clear-test-2", []PlanTaskInput{{Description: "T2"}})
423+ mustCreatePlans(t, store, "clear-test-1", "clear-test-2")
424424
425425 // Verify plans exist
426426 plans, _, _ := store.ListPlans()
@@ -444,8 +444,7 @@ func TestPlanStore_ClearAllPlans(t *testing.T) {
444444 store := planStore
445445
446446 // Create some plans
447- store.CreatePlan("all-clear-1", []PlanTaskInput{{Description: "T1"}})
448- store.CreatePlan("all-clear-2", []PlanTaskInput{{Description: "T2"}})
447+ mustCreatePlans(t, store, "all-clear-1", "all-clear-2")
449448
450449 // Clear all
451450 store.ClearAllPlans()
@@ -540,3 +539,13 @@ func TestPlanTool_CreateAndStatus(t *testing.T) {
540539 // The store tests above verify the core functionality
541540 t.Skip("Skipping tool integration test - requires full Genkit setup")
542541 }
542+
543+// mustCreatePlans creates one single-task plan per name, failing the test on error.
544+func mustCreatePlans(t *testing.T, store *PlanStore, names ...string) {
545+ t.Helper()
546+ for _, name := range names {
547+ if _, _, err := store.CreatePlan(name, []PlanTaskInput{{Description: "task of " + name}}); err != nil {
548+ t.Fatalf("CreatePlan %s failed: %v", name, err)
549+ }
550+ }
551+}
@@ -227,7 +227,7 @@ func TestPlanStore_NextTask(t *testing.T) {
227 }227 }
228 228
229 // Second next - should mark task 1 as done, go to task 2229 // Second next - should mark task 1 as done, go to task 2
230- plan, nextTask, msg, err = store.NextTask("next-test")230+ plan, nextTask, _, err = store.NextTask("next-test")
231 if err != nil {231 if err != nil {
232 t.Fatalf("Second NextTask failed: %v", err)232 t.Fatalf("Second NextTask failed: %v", err)
233 }233 }
@@ -242,7 +242,7 @@ func TestPlanStore_NextTask(t *testing.T) {
242 }242 }
243 243
244 // Third next - should mark task 2 as done, go to task 3244 // Third next - should mark task 2 as done, go to task 3
245- plan, nextTask, msg, err = store.NextTask("next-test")245+ plan, nextTask, _, err = store.NextTask("next-test")
246 if err != nil {246 if err != nil {
247 t.Fatalf("Third NextTask failed: %v", err)247 t.Fatalf("Third NextTask failed: %v", err)
248 }248 }
@@ -362,9 +362,12 @@ func TestPlanStore_GetStatus(t *testing.T) {
362 }362 }
363 363
364 // Complete all tasks (need 3 more NextTask calls to complete all 3 tasks)364 // Complete all tasks (need 3 more NextTask calls to complete all 3 tasks)
365- store.NextTask("status-test") // Marks Task 1 as done, moves to Task 2365+ // Each call marks the current task done and moves on; the third reaches Current=4 (complete).
366- store.NextTask("status-test") // Marks Task 2 as done, moves to Task 3366+ for i := 0; i < 3; i++ {
367- store.NextTask("status-test") // Marks Task 3 as done, moves to Current=4 (complete)367+ if _, _, _, err := store.NextTask("status-test"); err != nil {
368+ t.Fatalf("NextTask %d failed: %v", i+1, err)
369+ }
370+ }
368 371
369 // Get status when complete372 // Get status when complete
370 statusLine, _, err = store.GetStatus("status-test")373 statusLine, _, err = store.GetStatus("status-test")
@@ -396,9 +399,7 @@ func TestPlanStore_ListPlans(t *testing.T) {
396 }399 }
397 400
398 // Create multiple plans401 // Create multiple plans
399- store.CreatePlan("plan-a", []PlanTaskInput{{Description: "A1"}})402+ mustCreatePlans(t, store, "plan-a", "plan-b", "plan-c")
400- store.CreatePlan("plan-b", []PlanTaskInput{{Description: "B1"}})
401- store.CreatePlan("plan-c", []PlanTaskInput{{Description: "C1"}})
402 403
403 // List plans404 // List plans
404 plans, msg, err = store.ListPlans()405 plans, msg, err = store.ListPlans()
@@ -419,8 +420,7 @@ func TestPlanStore_ClearPlans(t *testing.T) {
419 store := planStore420 store := planStore
420 421
421 // Create some plans422 // Create some plans
422- store.CreatePlan("clear-test-1", []PlanTaskInput{{Description: "T1"}})423+ mustCreatePlans(t, store, "clear-test-1", "clear-test-2")
423- store.CreatePlan("clear-test-2", []PlanTaskInput{{Description: "T2"}})
424 424
425 // Verify plans exist425 // Verify plans exist
426 plans, _, _ := store.ListPlans()426 plans, _, _ := store.ListPlans()
@@ -444,8 +444,7 @@ func TestPlanStore_ClearAllPlans(t *testing.T) {
444 store := planStore444 store := planStore
445 445
446 // Create some plans446 // Create some plans
447- store.CreatePlan("all-clear-1", []PlanTaskInput{{Description: "T1"}})447+ mustCreatePlans(t, store, "all-clear-1", "all-clear-2")
448- store.CreatePlan("all-clear-2", []PlanTaskInput{{Description: "T2"}})
449 448
450 // Clear all449 // Clear all
451 store.ClearAllPlans()450 store.ClearAllPlans()
@@ -540,3 +539,13 @@ func TestPlanTool_CreateAndStatus(t *testing.T) {
540 // The store tests above verify the core functionality539 // The store tests above verify the core functionality
541 t.Skip("Skipping tool integration test - requires full Genkit setup")540 t.Skip("Skipping tool integration test - requires full Genkit setup")
542 }541 }
542+
543+// mustCreatePlans creates one single-task plan per name, failing the test on error.
544+func mustCreatePlans(t *testing.T, store *PlanStore, names ...string) {
545+ t.Helper()
546+ for _, name := range names {
547+ if _, _, err := store.CreatePlan(name, []PlanTaskInput{{Description: "task of " + name}}); err != nil {
548+ t.Fatalf("CreatePlan %s failed: %v", name, err)
549+ }
550+ }
551+}
modified internal/tools/tools.go +1 -1
@@ -153,7 +153,7 @@ func runBash(ctx context.Context, dir, command string) (output string, failed bo
153153 // Unlinked at once: the descriptor stays valid for us and for whatever the
154154 // command leaves running, and nothing is left in /tmp if mm is killed.
155155 _ = os.Remove(sink.Name())
156- defer sink.Close()
156+ defer func() { _ = sink.Close() }()
157157
158158 cmd := exec.CommandContext(ctx, "bash", "-c", command)
159159 cmd.Dir = dir
@@ -153,7 +153,7 @@ func runBash(ctx context.Context, dir, command string) (output string, failed bo
153 // Unlinked at once: the descriptor stays valid for us and for whatever the153 // Unlinked at once: the descriptor stays valid for us and for whatever the
154 // command leaves running, and nothing is left in /tmp if mm is killed.154 // command leaves running, and nothing is left in /tmp if mm is killed.
155 _ = os.Remove(sink.Name())155 _ = os.Remove(sink.Name())
156- defer sink.Close()156+ defer func() { _ = sink.Close() }()
157 157
158 cmd := exec.CommandContext(ctx, "bash", "-c", command)158 cmd := exec.CommandContext(ctx, "bash", "-c", command)
159 cmd.Dir = dir159 cmd.Dir = dir
modified internal/tui/question.go +3 -2
@@ -286,13 +286,14 @@ func (v *View) drawFormChoices(p painter) {
286286 if at == f.cursor {
287287 mark = "❯ "
288288 }
289- if q.Kind == elicit.Multi {
289+ switch q.Kind {
290+ case elicit.Multi:
290291 box := "[ ] "
291292 if f.checked[at] {
292293 box = "[x] "
293294 }
294295 mark += box
295- } else if q.Kind == elicit.Choice {
296+ case elicit.Choice:
296297 mark += fmt.Sprintf("%d. ", at+1)
297298 }
298299 style := stBody
@@ -286,13 +286,14 @@ func (v *View) drawFormChoices(p painter) {
286 if at == f.cursor {286 if at == f.cursor {
287 mark = "❯ "287 mark = "❯ "
288 }288 }
289- if q.Kind == elicit.Multi {289+ switch q.Kind {
290+ case elicit.Multi:
290 box := "[ ] "291 box := "[ ] "
291 if f.checked[at] {292 if f.checked[at] {
292 box = "[x] "293 box = "[x] "
293 }294 }
294 mark += box295 mark += box
295- } else if q.Kind == elicit.Choice {296+ case elicit.Choice:
296 mark += fmt.Sprintf("%d. ", at+1)297 mark += fmt.Sprintf("%d. ", at+1)
297 }298 }
298 style := stBody299 style := stBody
modified internal/ui/ui.go +1 -1
@@ -132,7 +132,7 @@ func Notice(text string) {
132132 n.Notice(text)
133133 return
134134 }
135- fmt.Fprintln(Out, text)
135+ _, _ = fmt.Fprintln(Out, text)
136136 }
137137
138138 // Speaker is the optional side of a Sink that shows WHICH agent is answering:
@@ -132,7 +132,7 @@ func Notice(text string) {
132 n.Notice(text)132 n.Notice(text)
133 return133 return
134 }134 }
135- fmt.Fprintln(Out, text)135+ _, _ = fmt.Fprintln(Out, text)
136 }136 }
137 137
138 // Speaker is the optional side of a Sink that shows WHICH agent is answering:138 // Speaker is the optional side of a Sink that shows WHICH agent is answering: