modified
02-build-releases.sh +1 -0 | @@ -135,6 +135,7 @@ downloadTable() { | ||
| 135 | 135 | printf '| Platform | Download |\n|---|---|\n' |
| 136 | 136 | for platform in "${PLATFORMS[@]}"; do |
| 137 | 137 | local goos="${platform%/*}" goarch="${platform#*/}" |
| 138 | + # shellcheck disable=SC2016 # the backticks are Markdown code spans, not a command substitution | |
| 138 | 139 | printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")" |
| 139 | 140 | done |
| 140 | 141 | } |
| @@ -135,6 +135,7 @@ downloadTable() { | |||
| 135 | printf '| Platform | Download |\n|---|---|\n' | 135 | printf '| Platform | Download |\n|---|---|\n' |
| 136 | for platform in "${PLATFORMS[@]}"; do | 136 | 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 | done | 140 | 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 | ||
| 129 | 129 | | `demos/llmman/` | mm on llmman, and an agent that asks other agents | |
| 130 | 130 | | `demos/ollama/` | mm on Ollama | |
| 131 | 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 | 133 | | `demos/agents-md-file/` | an agent following a project's `AGENTS.md` | |
| 133 | 134 | | `demos/mini-mes/` | small teams of agents (`riker` coordinating a specifier and a coder, `gopher` a Go specialist) | |
| 134 | 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 | |
| @@ -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 | ||
| 39 | 39 | command=$rest |
| 40 | 40 | rewrite=1 |
| 41 | 41 | fi |
| 42 | + # shellcheck disable=SC2016 # '$(' is the literal text being refused, not an expansion | |
| 42 | 43 | case "$command" in |
| 43 | 44 | *';'*|*'&'*|*'|'*|*'`'*|*'$('*|*'>'*|*'<'*|*'\n'*) |
| 44 | 45 | deny "one command at a time, nothing chained or redirected. Your commands: $hint" ;; |
| @@ -39,6 +39,7 @@ case "$kind" in | |||
| 39 | command=$rest | 39 | command=$rest |
| 40 | rewrite=1 | 40 | rewrite=1 |
| 41 | fi | 41 | fi |
| 42 | + # shellcheck disable=SC2016 # '$(' is the literal text being refused, not an expansion | ||
| 42 | case "$command" in | 43 | 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() { | ||
| 135 | 135 | } |
| 136 | 136 | |
| 137 | 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 | 141 | not_after_attack |
| 140 | 142 | line=$(sed -n '/^## Encounter/,$p' "$SHEET" | grep -F -- "- $1:" | head -1 || true) |
| 141 | 143 | [ -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_attack | 141 | 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 | ||
| 2 | 2 | |
| 3 | 3 | go 1.26.0 |
| 4 | 4 | |
| 5 | +toolchain go1.26.9 | |
| 6 | + | |
| 5 | 7 | require ( |
| 6 | 8 | github.com/firebase/genkit/go v1.13.1 |
| 7 | 9 | github.com/gdamore/tcell/v2 v2.13.10 |
| @@ -10,19 +12,22 @@ require ( | ||
| 10 | 12 | go.lsp.dev/jsonrpc2 v1.0.1 |
| 11 | 13 | go.lsp.dev/protocol v1.0.1 |
| 12 | 14 | 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 | |
| 15 | 17 | gopkg.in/yaml.v3 v3.0.1 |
| 16 | 18 | ) |
| 17 | 19 | |
| 18 | 20 | require ( |
| 21 | + github.com/cespare/xxhash/v2 v2.3.0 // indirect | |
| 19 | 22 | github.com/gdamore/encoding v1.0.1 // indirect |
| 20 | 23 | github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect |
| 24 | + github.com/kr/text v0.2.0 // indirect | |
| 21 | 25 | github.com/lucasb-eyer/go-colorful v1.4.0 // indirect |
| 22 | 26 | github.com/pb33f/ordered-map/v2 v2.3.1 // indirect |
| 23 | 27 | github.com/rivo/uniseg v0.4.7 // indirect |
| 28 | + go.opentelemetry.io/otel/log v1.47.0 // indirect | |
| 24 | 29 | 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 | |
| 26 | 31 | ) |
| 27 | 32 | |
| 28 | 33 | require ( |
| @@ -30,7 +35,7 @@ require ( | ||
| 30 | 35 | github.com/buger/jsonparser v1.1.2 // indirect |
| 31 | 36 | github.com/coder/acp-go-sdk v0.13.5 |
| 32 | 37 | 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 | |
| 34 | 39 | github.com/go-logr/stdr v1.2.2 // indirect |
| 35 | 40 | github.com/goccy/go-yaml v1.19.2 // indirect |
| 36 | 41 | github.com/google/dotprompt/go v0.0.0-20260708220100-73beb993ac95 // indirect |
| @@ -45,10 +50,10 @@ require ( | ||
| 45 | 50 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect |
| 46 | 51 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect |
| 47 | 52 | 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 | |
| 54 | 59 | ) |
| @@ -2,6 +2,8 @@ module mm | |||
| 2 | 2 | ||
| 3 | go 1.26.0 | 3 | go 1.26.0 |
| 4 | 4 | ||
| 5 | +toolchain go1.26.9 | ||
| 6 | + | ||
| 5 | require ( | 7 | require ( |
| 6 | github.com/firebase/genkit/go v1.13.1 | 8 | github.com/firebase/genkit/go v1.13.1 |
| 7 | github.com/gdamore/tcell/v2 v2.13.10 | 9 | github.com/gdamore/tcell/v2 v2.13.10 |
| @@ -10,19 +12,22 @@ require ( | |||
| 10 | go.lsp.dev/jsonrpc2 v1.0.1 | 12 | go.lsp.dev/jsonrpc2 v1.0.1 |
| 11 | go.lsp.dev/protocol v1.0.1 | 13 | go.lsp.dev/protocol v1.0.1 |
| 12 | go.lsp.dev/uri v1.0.1 | 14 | go.lsp.dev/uri v1.0.1 |
| 13 | - golang.org/x/net v0.59.0 | 15 | + golang.org/x/net v0.61.0 |
| 14 | - golang.org/x/text v0.42.0 | 16 | + golang.org/x/text v0.43.0 |
| 15 | gopkg.in/yaml.v3 v3.0.1 | 17 | 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 // indirect | 22 | github.com/gdamore/encoding v1.0.1 // indirect |
| 20 | github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect | 23 | 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 // indirect | 25 | github.com/lucasb-eyer/go-colorful v1.4.0 // indirect |
| 22 | github.com/pb33f/ordered-map/v2 v2.3.1 // indirect | 26 | github.com/pb33f/ordered-map/v2 v2.3.1 // indirect |
| 23 | github.com/rivo/uniseg v0.4.7 // indirect | 27 | 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 // indirect | 29 | 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 |
| 26 | ) | 31 | ) |
| 27 | 32 | ||
| 28 | require ( | 33 | require ( |
| @@ -30,7 +35,7 @@ require ( | |||
| 30 | github.com/buger/jsonparser v1.1.2 // indirect | 35 | github.com/buger/jsonparser v1.1.2 // indirect |
| 31 | github.com/coder/acp-go-sdk v0.13.5 | 36 | github.com/coder/acp-go-sdk v0.13.5 |
| 32 | github.com/coder/websocket v1.8.14 // indirect | 37 | 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 |
| 34 | github.com/go-logr/stdr v1.2.2 // indirect | 39 | github.com/go-logr/stdr v1.2.2 // indirect |
| 35 | github.com/goccy/go-yaml v1.19.2 // indirect | 40 | github.com/goccy/go-yaml v1.19.2 // indirect |
| 36 | github.com/google/dotprompt/go v0.0.0-20260708220100-73beb993ac95 // indirect | 41 | 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 // indirect | 50 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect |
| 46 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect | 51 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect |
| 47 | github.com/yosida95/uritemplate/v3 v3.0.2 // indirect | 52 | github.com/yosida95/uritemplate/v3 v3.0.2 // indirect |
| 48 | - go.opentelemetry.io/auto/sdk v1.1.0 // indirect | 53 | + go.opentelemetry.io/auto/sdk v1.2.1 // indirect |
| 49 | - go.opentelemetry.io/otel v1.36.0 // indirect | 54 | + go.opentelemetry.io/otel v1.47.0 // indirect |
| 50 | - go.opentelemetry.io/otel/metric v1.36.0 // indirect | 55 | + go.opentelemetry.io/otel/metric v1.47.0 // indirect |
| 51 | - go.opentelemetry.io/otel/sdk v1.36.0 // indirect | 56 | + go.opentelemetry.io/otel/sdk v1.47.0 // indirect |
| 52 | - go.opentelemetry.io/otel/trace v1.36.0 // indirect | 57 | + go.opentelemetry.io/otel/trace v1.47.0 // indirect |
| 53 | - golang.org/x/sys v0.48.0 // indirect | 58 | + 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 | ||
| 2 | 2 | github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= |
| 3 | 3 | github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= |
| 4 | 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 | 7 | github.com/coder/acp-go-sdk v0.13.5 h1:LI9jq5xon7xslaYlnoktvTVyDlE37yIk2daT7N9ASYk= |
| 6 | 8 | github.com/coder/acp-go-sdk v0.13.5/go.mod h1:yKzM/3R9uELp4+nBAwwtkS0aN1FOFjo11CNPy37yFko= |
| 7 | 9 | github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= |
| 8 | 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 | 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 | 13 | github.com/firebase/genkit/go v1.13.1 h1:6fgQ0ogxG+SIgtYQD/yf8T0+39lacioB5voFdFYk1tI= |
| 13 | 14 | github.com/firebase/genkit/go v1.13.1/go.mod h1:nWewix7d2O+oikJ035XPmY4mrO2phXwmk6NekKmaDLU= |
| 14 | 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 | 19 | github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg= |
| 19 | 20 | github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= |
| 20 | 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.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= | |
| 23 | 24 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= |
| 24 | 25 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= |
| 25 | 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 | 45 | github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= |
| 45 | 46 | github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= |
| 46 | 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 | 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
| 49 | 49 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= |
| 50 | 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 | 54 | github.com/sashabaranov/go-openai v1.42.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= |
| 55 | 55 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
| 56 | 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= | |
| 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= | |
| 59 | 59 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= |
| 60 | 60 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= |
| 61 | 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 | 82 | go.lsp.dev/protocol v1.0.1/go.mod h1:e//i41aEqc9ml32xiNuneUwbjOjuvTA4+uv+AkssJgk= |
| 83 | 83 | go.lsp.dev/uri v1.0.1 h1:ywxJN1UXoQ+v0I9lkoQNMmsm7lub5G9ZG7KXT8YTx30= |
| 84 | 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= | |
| 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= | |
| 95 | 99 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= |
| 96 | 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 | 103 | go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= |
| 98 | 104 | go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= |
| 99 | 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 | 112 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= |
| 107 | 113 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= |
| 108 | 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= | |
| 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= | |
| 111 | 117 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
| 112 | 118 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
| 113 | 119 | 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= | |
| 116 | 122 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
| 117 | 123 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
| 118 | 124 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
| 119 | 125 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
| 120 | 126 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
| 121 | 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= | |
| 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= | |
| 124 | 130 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
| 125 | 131 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |
| 126 | 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= | |
| 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= | |
| 129 | 135 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
| 130 | 136 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
| 131 | 137 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= |
| 132 | 138 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= |
| 133 | 139 | 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= | |
| 136 | 142 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
| 137 | 143 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
| 138 | 144 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= |
| 139 | 145 | 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= | |
| 142 | 148 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
| 143 | 149 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
| 144 | 150 | 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. | ||
| 571 | 571 | return |
| 572 | 572 | } |
| 573 | 573 | |
| 574 | - // Process and display the result | |
| 575 | - messages = session.AfterTurn(messages, before, resp, history, err) | |
| 576 | - | |
| 577 | 574 | // Display command stats like the regular mode |
| 578 | 575 | turn := history[min(before, len(history)):] |
| 579 | 576 | cmds := engine.CommandList(turn) |
| @@ -571,9 +571,6 @@ func RunPrompt(ctx context.Context, e *engine.Engine, system string, tools []ai. | |||
| 571 | return | 571 | 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 mode | 574 | // 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 { | ||
| 19 | 19 | t.Helper() |
| 20 | 20 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 21 | 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)) | |
| 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") | |
| 25 | 25 | })) |
| 26 | 26 | t.Cleanup(srv.Close) |
| 27 | 27 | 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 ( | ||
| 12 | 12 | |
| 13 | 13 | func ExampleSystem() { |
| 14 | 14 | dir, _ := os.MkdirTemp("", "project") |
| 15 | - defer os.RemoveAll(dir) | |
| 15 | + defer func() { _ = os.RemoveAll(dir) }() | |
| 16 | 16 | _ = os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("Run `make test` before committing."), 0o644) |
| 17 | 17 | config.Cfg.AgentsMd = config.AgentsMdConfig{Enabled: true, Path: "AGENTS.md"} |
| 18 | 18 | |
| @@ -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 | ||
| 89 | 89 | if req.WithContext { |
| 90 | 90 | label += ", with the conversation" |
| 91 | 91 | } |
| 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) } | |
| 94 | 94 | // With postAsk hooks the answer is held back until they have run: what |
| 95 | 95 | // stdout carries is what the caller reads, and a hook may replace it. |
| 96 | 96 | var held strings.Builder |
| @@ -103,10 +103,10 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde | ||
| 103 | 103 | answer := res.Answer |
| 104 | 104 | if !streamed { |
| 105 | 105 | answer = res.Text() |
| 106 | - fmt.Fprint(stdout, answer) | |
| 106 | + _, _ = fmt.Fprint(stdout, answer) | |
| 107 | 107 | } |
| 108 | 108 | if answer != "" && !strings.HasSuffix(answer, "\n") { |
| 109 | - fmt.Fprintln(stdout) | |
| 109 | + _, _ = fmt.Fprintln(stdout) | |
| 110 | 110 | } |
| 111 | 111 | if err == nil && res.Answer == "" { |
| 112 | 112 | return errors.New(req.Agent + " gave no answer") |
| @@ -135,14 +135,14 @@ type cliSink struct { | ||
| 135 | 135 | |
| 136 | 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 | 139 | func (s *cliSink) Reasoning(string) {} |
| 140 | 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 | 143 | func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) { |
| 144 | 144 | if res.Failed { |
| 145 | - fmt.Fprintln(s.log, " │ failed") | |
| 145 | + _, _ = fmt.Fprintln(s.log, " │ failed") | |
| 146 | 146 | } |
| 147 | 147 | } |
| 148 | 148 | 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: what | 94 | // 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.Builder | 96 | var held strings.Builder |
| @@ -103,10 +103,10 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde | |||
| 103 | answer := res.Answer | 103 | 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 { | ||
| 335 | 335 | func TestCompactThroughEngineSummarize(t *testing.T) { |
| 336 | 336 | g := genkit.Init(context.Background()) |
| 337 | 337 | var sawTools int |
| 338 | - genkit.DefineModel(g, "dmr/"+config.Cfg.Model, | |
| 338 | + genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model, | |
| 339 | 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 | 341 | sawTools = len(r.Tools) |
| 342 | 342 | return &ai.ModelResponse{ |
| 343 | 343 | 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 int | 337 | 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 { | ||
| 29 | 29 | 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 | 51 | // loadedWindow reads /api/ps, one level above /v1, and returns the served |
| 33 | 52 | // context of `model` — 0 when it is not loaded or the route does not answer. |
| 34 | 53 | func loadedWindow(baseURL, model string) int { |
| @@ -29,6 +29,25 @@ func (l modelList) has(id string) bool { | |||
| 29 | return false | 29 | 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 served | 51 | // 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 | ||
| 297 | 297 | // Text the broken attempt already streamed is not taken back. |
| 298 | 298 | if streamBroken(err) && ctx.Err() == nil { |
| 299 | 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 | 301 | pl.expect() |
| 302 | 302 | resp, err = next(ctx, params) |
| 303 | 303 | err = withServerMessage(ctx, err) |
| @@ -321,7 +321,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a | ||
| 321 | 321 | } |
| 322 | 322 | // ui.Out: stderr in ACP mode, never the JSON-RPC stream. |
| 323 | 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 | 325 | what, lastRunes(err.Error(), 60), argRetries, maxArgRetries) |
| 326 | 326 | params.Request.Messages = append(slices.Clone(params.Request.Messages), argsRetryNote(truncated)) |
| 327 | 327 | 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) | ||
| 32 | 32 | g := genkit.Init(context.Background()) |
| 33 | 33 | |
| 34 | 34 | calls := 0 |
| 35 | - genkit.DefineModel(g, "dmr/"+config.Cfg.Model, | |
| 35 | + genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model, | |
| 36 | 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 | 38 | if calls >= len(turns) { |
| 39 | 39 | t.Errorf("the model was called %d times for %d planned turns", calls+1, len(turns)) |
| 40 | 40 | return turns[len(turns)-1], nil |
| @@ -301,9 +301,9 @@ func TestSummarize(t *testing.T) { | ||
| 301 | 301 | var sawTools int |
| 302 | 302 | answers := []string{" ## Goal\nnotes\n", ""} |
| 303 | 303 | call := 0 |
| 304 | - genkit.DefineModel(g, "dmr/"+config.Cfg.Model, | |
| 304 | + genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model, | |
| 305 | 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 | 307 | sawTools = len(r.Tools) |
| 308 | 308 | a := answers[call] |
| 309 | 309 | 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 := 0 | 34 | 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], nil | 40 | return turns[len(turns)-1], nil |
| @@ -301,9 +301,9 @@ func TestSummarize(t *testing.T) { | |||
| 301 | var sawTools int | 301 | var sawTools int |
| 302 | answers := []string{" ## Goal\nnotes\n", ""} | 302 | answers := []string{" ## Goal\nnotes\n", ""} |
| 303 | call := 0 | 303 | 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 ( | ||
| 16 | 16 | // pulls it, which can take minutes — worth a warning, because the wait looks |
| 17 | 17 | // like a stalled server. |
| 18 | 18 | 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)) | |
| 33 | 20 | } |
| @@ -16,18 +16,5 @@ import ( | |||
| 16 | // pulls it, which can take minutes — worth a warning, because the wait looks | 16 | // 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 modelList | 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)) |
| 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 ( | ||
| 10 | 10 | // for a loaded qwen3.5:2b). Unlike llmman, Ollama's OpenAI API does NOT pull |
| 11 | 11 | // a missing model: the request fails with a 404, so the warning says to pull. |
| 12 | 12 | 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)) | |
| 27 | 14 | } |
| @@ -10,18 +10,5 @@ import ( | |||
| 10 | // for a loaded qwen3.5:2b). Unlike llmman, Ollama's OpenAI API does NOT pull | 10 | // 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 modelList | 13 | + 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 { | ||
| 268 | 268 | if err != nil { |
| 269 | 269 | return false |
| 270 | 270 | } |
| 271 | - defer resp.Body.Close() | |
| 271 | + defer func() { _ = resp.Body.Close() }() | |
| 272 | 272 | return resp.StatusCode == http.StatusOK |
| 273 | 273 | } |
| 274 | 274 | |
| @@ -279,7 +279,7 @@ func getJSON(url string, out any) bool { | ||
| 279 | 279 | if err != nil { |
| 280 | 280 | return false |
| 281 | 281 | } |
| 282 | - defer resp.Body.Close() | |
| 282 | + defer func() { _ = resp.Body.Close() }() | |
| 283 | 283 | if resp.StatusCode != http.StatusOK { |
| 284 | 284 | return false |
| 285 | 285 | } |
| @@ -268,7 +268,7 @@ func reachable(baseURL string) bool { | |||
| 268 | if err != nil { | 268 | if err != nil { |
| 269 | return false | 269 | return false |
| 270 | } | 270 | } |
| 271 | - defer resp.Body.Close() | 271 | + defer func() { _ = resp.Body.Close() }() |
| 272 | return resp.StatusCode == http.StatusOK | 272 | 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 false | 280 | 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 false | 284 | 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 | ||
| 146 | 146 | spinner.Stop() |
| 147 | 147 | // ui.Out, not stdout: in ACP mode this diagnosis belongs to |
| 148 | 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 | 150 | cancel() |
| 151 | 151 | return |
| 152 | 152 | } |
| @@ -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 to | 147 | // 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 | return | 151 | return |
| 152 | } | 152 | } |
modified
internal/engine/progress_test.go +6 -6 | @@ -47,7 +47,7 @@ func timeouts(t *testing.T, between, first time.Duration) { | ||
| 47 | 47 | func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgress *bool) string { |
| 48 | 48 | srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) { |
| 49 | 49 | fl := w.(http.Flusher) |
| 50 | - fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, "")) | |
| 50 | + _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, "")) | |
| 51 | 51 | fl.Flush() |
| 52 | 52 | for start := time.Now(); time.Since(start) < reading; { |
| 53 | 53 | if every == 0 { |
| @@ -55,15 +55,15 @@ func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgres | ||
| 55 | 55 | break |
| 56 | 56 | } |
| 57 | 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 | 59 | fl.Flush() |
| 60 | 60 | } |
| 61 | - fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, "")) | |
| 61 | + _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, "")) | |
| 62 | 62 | fl.Flush() |
| 63 | 63 | 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") | |
| 67 | 67 | }) |
| 68 | 68 | if sawProgress != nil { |
| 69 | 69 | // 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 | break | 55 | 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) { | ||
| 110 | 110 | t.Errorf("%s: %v", who, err) |
| 111 | 111 | return |
| 112 | 112 | } |
| 113 | - defer resp.Body.Close() | |
| 113 | + defer func() { _ = resp.Body.Close() }() | |
| 114 | 114 | _, _ = io.ReadAll(resp.Body) |
| 115 | 115 | } |
| 116 | 116 | |
| @@ -175,10 +175,10 @@ var _ = option.WithMiddleware(reasoningTap) // the middleware still fits the opt | ||
| 175 | 175 | // request. |
| 176 | 176 | func TestReasoningStaysOutOfTheHistory(t *testing.T) { |
| 177 | 177 | 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") | |
| 182 | 182 | }) |
| 183 | 183 | e := engineOn(t, srv.URL+"/v1") |
| 184 | 184 | |
| @@ -213,13 +213,13 @@ func TestReasoningStreamsAsOneBlock(t *testing.T) { | ||
| 213 | 213 | // the tap reads them all before Genkit's callback runs and the bug |
| 214 | 214 | // does not show. |
| 215 | 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 | 217 | w.(http.Flusher).Flush() |
| 218 | 218 | time.Sleep(20 * time.Millisecond) |
| 219 | 219 | } |
| 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") | |
| 223 | 223 | }) |
| 224 | 224 | e := engineOn(t, srv.URL+"/v1") |
| 225 | 225 | 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 | return | 111 | 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 bug | 213 | // 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.Builder | 225 | 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 | ||
| 38 | 38 | } |
| 39 | 39 | |
| 40 | 40 | 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") | |
| 44 | 44 | } |
| 45 | 45 | |
| 46 | 46 | // llama.cpp's way of failing mid-stream: an `error:` event, after 200 and |
| 47 | 47 | // some content. |
| 48 | 48 | 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") | |
| 51 | 51 | } |
| 52 | 52 | |
| 53 | 53 | func engineOn(t *testing.T, baseURL string) *Engine { |
| @@ -101,10 +101,10 @@ func TestServerErrorEventIsExplained(t *testing.T) { | ||
| 101 | 101 | // line that says what happened. |
| 102 | 102 | func TestCutStreamIsExplained(t *testing.T) { |
| 103 | 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 | 105 | w.(http.Flusher).Flush() |
| 106 | 106 | conn, _, _ := w.(http.Hijacker).Hijack() |
| 107 | - conn.Close() | |
| 107 | + _ = conn.Close() | |
| 108 | 108 | }) |
| 109 | 109 | e := engineOn(t, srv.URL+"/v1") |
| 110 | 110 | _, _, 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 and | 46 | // 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) { | ||
| 31 | 31 | // answering streams content as the model's whole answer. |
| 32 | 32 | func answering(content string) func(http.ResponseWriter) { |
| 33 | 33 | 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") | |
| 37 | 37 | } |
| 38 | 38 | } |
| 39 | 39 | |
| @@ -140,9 +140,9 @@ func TestPropertiesReachTheServerInDeclarationOrder(t *testing.T) { | ||
| 140 | 140 | func TestACutStructuredAnswerNamesMaxTokens(t *testing.T) { |
| 141 | 141 | withOutputSchema(t, "var Names []string") |
| 142 | 142 | 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") | |
| 146 | 146 | }) |
| 147 | 147 | _, _, err := engineOn(t, srv.URL+"/v1").Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("?")}, nil) |
| 148 | 148 | 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) { | ||
| 106 | 106 | // and names the setting — the session file only ever said "empty summary". |
| 107 | 107 | func TestEmptySummarySaysWhy(t *testing.T) { |
| 108 | 108 | 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") | |
| 112 | 112 | }) |
| 113 | 113 | e := engineOn(t, srv.URL+"/v1") |
| 114 | 114 | |
| @@ -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) { | ||
| 41 | 41 | // without it the marker lands on the spinner's line and the next |
| 42 | 42 | // frame erases it. |
| 43 | 43 | spinner.Stop() |
| 44 | - fmt.Fprint(ui.Out, "\n💭 ") | |
| 44 | + _, _ = fmt.Fprint(ui.Out, "\n💭 ") | |
| 45 | 45 | t.open = true |
| 46 | 46 | } |
| 47 | - fmt.Fprint(ui.Out, dim(text)) | |
| 47 | + _, _ = fmt.Fprint(ui.Out, dim(text)) | |
| 48 | 48 | } |
| 49 | 49 | |
| 50 | 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 | 60 | return |
| 61 | 61 | } |
| 62 | 62 | t.open = false |
| 63 | - fmt.Fprintln(ui.Out) | |
| 63 | + _, _ = fmt.Fprintln(ui.Out) | |
| 64 | 64 | } |
| 65 | 65 | |
| 66 | 66 | // 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 next | 41 | // 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 = true | 45 | 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 | return | 60 | return |
| 61 | } | 61 | } |
| 62 | t.open = false | 62 | 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 different | 66 | // 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 ( | ||
| 14 | 14 | |
| 15 | 15 | // The plugin's error for a tool call cut off by max_tokens, as it was seen on |
| 16 | 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 | 19 | // scriptedGenkit registers a model that plays `steps` in order — a response |
| 20 | 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 | 22 | t.Helper() |
| 23 | 23 | g := genkit.Init(context.Background()) |
| 24 | 24 | var seen []*ai.ModelRequest |
| 25 | - genkit.DefineModel(g, "dmr/"+config.Cfg.Model, | |
| 25 | + genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model, | |
| 26 | 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 | 28 | seen = append(seen, req) |
| 29 | 29 | if len(seen) > len(steps) { |
| 30 | 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 | 51 | // front end keeps, where the model will read it again for the next piece. |
| 52 | 52 | func TestTruncatedToolCallIsHandedBack(t *testing.T) { |
| 53 | 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 | 56 | resp, full, err := testEngine(g).Generate(context.Background(), |
| 57 | 57 | []*ai.Message{ai.NewUserTextMessage("write the document")}, |
| @@ -92,9 +92,9 @@ func TestTruncatedToolCallGivesUpAndExplains(t *testing.T) { | ||
| 92 | 92 | config.Cfg.Sampling = map[string]any{"max_tokens": 4096} |
| 93 | 93 | defer func() { config.Cfg.Sampling = old }() |
| 94 | 94 | |
| 95 | - steps := []any{truncatedArgs} | |
| 95 | + steps := []any{errTruncatedArgs} | |
| 96 | 96 | for range maxArgRetries { |
| 97 | - steps = append(steps, truncatedArgs) | |
| 97 | + steps = append(steps, errTruncatedArgs) | |
| 98 | 98 | } |
| 99 | 99 | g, seen := scriptedGenkit(t, steps...) |
| 100 | 100 | e := testEngine(g) |
| @@ -120,7 +120,7 @@ func TestArgsError(t *testing.T) { | ||
| 120 | 120 | }{ |
| 121 | 121 | {nil, false, false}, |
| 122 | 122 | {errors.New("connection refused"), false, false}, |
| 123 | - {truncatedArgs, true, false}, | |
| 123 | + {errTruncatedArgs, true, false}, | |
| 124 | 124 | {errors.New(`could not parse tool args: unmarshal failed to parse json string {"a":1}}: invalid character '}'`), false, true}, |
| 125 | 125 | } |
| 126 | 126 | 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 on | 15 | // 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 response | 19 | // 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.ModelRequest | 24 | 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 { | ||
| 51 | 51 | where := traceDirOf(dir) |
| 52 | 52 | base := filepath.Join(where, fmt.Sprintf("%s-%s-%04d", time.Now().Format("20060102-150405.000"), agent, traceSeq.Add(1))) |
| 53 | 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 | 58 | if req.Body != nil { |
| 57 | 59 | 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) { | ||
| 116 | 116 | if devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil { |
| 117 | 117 | saved := os.Stdout |
| 118 | 118 | 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 | 122 | 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.Stdout | 117 | saved := os.Stdout |
| 118 | os.Stdout = devnull | 118 | 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) { | ||
| 296 | 296 | } |
| 297 | 297 | } |
| 298 | 298 | 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 | + } | |
| 301 | 304 | run("apply", "p.diff") |
| 302 | 305 | if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new { |
| 303 | 306 | 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 { | ||
| 100 | 100 | if err != nil { |
| 101 | 101 | return err |
| 102 | 102 | } |
| 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 | |
| 104 | 104 | |
| 105 | 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 | 107 | return err |
| 108 | 108 | } |
| 109 | 109 | 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 err | 101 | return err |
| 102 | } | 102 | } |
| 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 |
| 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 err | 107 | 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 { | ||
| 82 | 82 | } |
| 83 | 83 | case protocol.String: |
| 84 | 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 | 86 | if v != nil { |
| 87 | 87 | text = v.Value |
| 88 | 88 | } |
| @@ -97,11 +97,11 @@ func hoverText(c protocol.HoverContents) string { | ||
| 97 | 97 | } |
| 98 | 98 | |
| 99 | 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 | 101 | switch v := m.(type) { |
| 102 | 102 | case protocol.String: |
| 103 | 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 | 105 | if v != nil { |
| 106 | 106 | return v.Value |
| 107 | 107 | } |
| @@ -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.Value | 87 | 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.Value | 106 | 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 | ||
| 188 | 188 | // Perform discovery |
| 189 | 189 | tools, err := client.ListTools(ctx) |
| 190 | 190 | if err != nil { |
| 191 | - client.Close() | |
| 191 | + _ = client.Close() | |
| 192 | 192 | warnings = append(warnings, fmt.Sprintf("mcp server %s: discovery failed: %v", server.Name, err)) |
| 193 | 193 | continue |
| 194 | 194 | } |
| @@ -230,6 +230,6 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se | ||
| 230 | 230 | // CloseAll closes all clients. |
| 231 | 231 | func CloseAll(clients map[string]*ServerClient) { |
| 232 | 232 | for _, sc := range clients { |
| 233 | - sc.Close() | |
| 233 | + _ = sc.Close() | |
| 234 | 234 | } |
| 235 | 235 | } |
| @@ -188,7 +188,7 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se | |||
| 188 | // Perform discovery | 188 | // 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 | continue | 193 | 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 | ||
| 170 | 170 | func parseCallResult(raw json.RawMessage) (CallToolResult, error) { |
| 171 | 171 | var many callToolResponse |
| 172 | 172 | if err := json.Unmarshal(raw, &many); err == nil { |
| 173 | - return CallToolResult{Content: many.Content, IsError: many.IsError}, nil | |
| 173 | + return CallToolResult(many), nil | |
| 174 | 174 | } |
| 175 | 175 | var single struct { |
| 176 | 176 | Content Content `json:"content"` |
| @@ -252,7 +252,7 @@ func (c *HTTPClient) request(ctx context.Context, method string, params any) (*m | ||
| 252 | 252 | if err != nil { |
| 253 | 253 | return nil, err |
| 254 | 254 | } |
| 255 | - defer httpResp.Body.Close() | |
| 255 | + defer func() { _ = httpResp.Body.Close() }() | |
| 256 | 256 | |
| 257 | 257 | resp, err := readResponse(httpResp, id) |
| 258 | 258 | if err != nil { |
| @@ -339,7 +339,7 @@ func (c *HTTPClient) Close() error { | ||
| 339 | 339 | } |
| 340 | 340 | _ = c.setHeaders(req) |
| 341 | 341 | if resp, err := c.httpClient.Do(req); err == nil { |
| 342 | - resp.Body.Close() | |
| 342 | + _ = resp.Body.Close() | |
| 343 | 343 | } |
| 344 | 344 | c.resetSession() |
| 345 | 345 | 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 callToolResponse | 171 | 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}, nil | 173 | + 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, err | 253 | 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 nil | 345 | 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 | ||
| 97 | 97 | } |
| 98 | 98 | w.Header().Set("Content-Type", "text/event-stream") |
| 99 | 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") | |
| 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) | |
| 102 | 102 | } |
| 103 | 103 | |
| 104 | 104 | func newTestClient(t *testing.T, url string) *HTTPClient { |
| @@ -198,10 +198,10 @@ func TestHTTPClientFallsBackWithoutInitialize(t *testing.T) { | ||
| 198 | 198 | _ = json.NewDecoder(r.Body).Decode(&msg) |
| 199 | 199 | w.Header().Set("Content-Type", "application/json") |
| 200 | 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 | 202 | 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 | 206 | defer ts.Close() |
| 207 | 207 | |
| @@ -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 | return | 202 | 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) { | ||
| 21 | 21 | if resp.StatusCode >= 200 && resp.StatusCode <= 299 { |
| 22 | 22 | return resp, nil |
| 23 | 23 | } |
| 24 | - defer resp.Body.Close() | |
| 24 | + defer func() { _ = resp.Body.Close() }() | |
| 25 | 25 | snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) |
| 26 | 26 | return nil, &httpStatusError{Status: resp.StatusCode, Body: strings.TrimSpace(string(snippet))} |
| 27 | 27 | } |
| @@ -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, nil | 22 | 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( | ||
| 405 | 405 | output.WriteString(content.Text) |
| 406 | 406 | } else { |
| 407 | 407 | // 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 | |
| @@ -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 warning | 407 | // 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 @@ | ||
| 1 | 1 | package tools |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | - "context" | |
| 5 | - "fmt" | |
| 6 | - | |
| 7 | 4 | "mm/internal/config" |
| 8 | 5 | "mm/internal/detector" |
| 9 | 6 | "mm/internal/mcp" |
| 10 | - "mm/internal/spinner" | |
| 11 | - "mm/internal/ui" | |
| 12 | 7 | |
| 13 | 8 | "github.com/firebase/genkit/go/ai" |
| 14 | 9 | "github.com/firebase/genkit/go/genkit" |
| @@ -35,90 +30,3 @@ func MCP( | ||
| 35 | 30 | // Wrap all tools and get their names |
| 36 | 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 | -} | |
| @@ -1,14 +1,9 @@ | |||
| 1 | package tools | 1 | 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 names | 30 | // 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) { | ||
| 351 | 351 | if plan.Current == 0 { |
| 352 | 352 | // Not started, show Task 1 |
| 353 | 353 | 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), | |
| 363 | 355 | fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil |
| 364 | 356 | } |
| 365 | 357 | |
| 366 | 358 | // Current > 0, show Task Current |
| 367 | 359 | if plan.Current <= len(plan.Tasks) { |
| 368 | 360 | 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), | |
| 381 | 362 | 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 | 367 | 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 | 384 | // ClearPlans removes all plans for the current session. |
| 390 | 385 | // Called when /new command is issued. |
| 391 | 386 | func (s *PlanStore) ClearPlans() { |
| @@ -402,15 +397,6 @@ func (s *PlanStore) ClearAllPlans() { | ||
| 402 | 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 | 400 | // PlanTool declares the `plan` tool. |
| 415 | 401 | func PlanTool(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef { |
| 416 | 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 | 445 | 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) | |
| 555 | 447 | |
| 556 | 448 | // Record in loop detector |
| 557 | 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 | 487 | s.ToolRunning(ev.ID) |
| 596 | 488 | |
| 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) | |
| 685 | 490 | |
| 686 | 491 | // Check if failed |
| 687 | 492 | 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 1 | 352 | // 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)), nil | 355 | fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil |
| 364 | } | 356 | } |
| 365 | 357 | ||
| 366 | // Current > 0, show Task Current | 358 | // 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)), nil | 362 | 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)), nil | 367 | 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 detector | 448 | // 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 action | 489 | + 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 failed | 491 | // 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) { | ||
| 227 | 227 | } |
| 228 | 228 | |
| 229 | 229 | // 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 | 231 | if err != nil { |
| 232 | 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 | 244 | // 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 | 246 | if err != nil { |
| 247 | 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 | 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 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 | + } | |
| 368 | 371 | |
| 369 | 372 | // Get status when complete |
| 370 | 373 | statusLine, _, err = store.GetStatus("status-test") |
| @@ -396,9 +399,7 @@ func TestPlanStore_ListPlans(t *testing.T) { | ||
| 396 | 399 | } |
| 397 | 400 | |
| 398 | 401 | // 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") | |
| 402 | 403 | |
| 403 | 404 | // List plans |
| 404 | 405 | plans, msg, err = store.ListPlans() |
| @@ -419,8 +420,7 @@ func TestPlanStore_ClearPlans(t *testing.T) { | ||
| 419 | 420 | store := planStore |
| 420 | 421 | |
| 421 | 422 | // 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") | |
| 424 | 424 | |
| 425 | 425 | // Verify plans exist |
| 426 | 426 | plans, _, _ := store.ListPlans() |
| @@ -444,8 +444,7 @@ func TestPlanStore_ClearAllPlans(t *testing.T) { | ||
| 444 | 444 | store := planStore |
| 445 | 445 | |
| 446 | 446 | // 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") | |
| 449 | 448 | |
| 450 | 449 | // Clear all |
| 451 | 450 | store.ClearAllPlans() |
| @@ -540,3 +539,13 @@ func TestPlanTool_CreateAndStatus(t *testing.T) { | ||
| 540 | 539 | // The store tests above verify the core functionality |
| 541 | 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 | +} | |
| @@ -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 2 | 229 | // 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 3 | 244 | // 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 2 | 365 | + // 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 3 | 366 | + 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 complete | 372 | // 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 plans | 401 | // 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 plans | 404 | // 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 := planStore | 420 | store := planStore |
| 420 | 421 | ||
| 421 | // Create some plans | 422 | // 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 exist | 425 | // Verify plans exist |
| 426 | plans, _, _ := store.ListPlans() | 426 | plans, _, _ := store.ListPlans() |
| @@ -444,8 +444,7 @@ func TestPlanStore_ClearAllPlans(t *testing.T) { | |||
| 444 | store := planStore | 444 | store := planStore |
| 445 | 445 | ||
| 446 | // Create some plans | 446 | // 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 all | 449 | // 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 functionality | 539 | // 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 | ||
| 153 | 153 | // Unlinked at once: the descriptor stays valid for us and for whatever the |
| 154 | 154 | // command leaves running, and nothing is left in /tmp if mm is killed. |
| 155 | 155 | _ = os.Remove(sink.Name()) |
| 156 | - defer sink.Close() | |
| 156 | + defer func() { _ = sink.Close() }() | |
| 157 | 157 | |
| 158 | 158 | cmd := exec.CommandContext(ctx, "bash", "-c", command) |
| 159 | 159 | 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 the | 153 | // 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 = dir | 159 | cmd.Dir = dir |
modified
internal/tui/question.go +3 -2 | @@ -286,13 +286,14 @@ func (v *View) drawFormChoices(p painter) { | ||
| 286 | 286 | if at == f.cursor { |
| 287 | 287 | mark = "❯ " |
| 288 | 288 | } |
| 289 | - if q.Kind == elicit.Multi { | |
| 289 | + switch q.Kind { | |
| 290 | + case elicit.Multi: | |
| 290 | 291 | box := "[ ] " |
| 291 | 292 | if f.checked[at] { |
| 292 | 293 | box = "[x] " |
| 293 | 294 | } |
| 294 | 295 | mark += box |
| 295 | - } else if q.Kind == elicit.Choice { | |
| 296 | + case elicit.Choice: | |
| 296 | 297 | mark += fmt.Sprintf("%d. ", at+1) |
| 297 | 298 | } |
| 298 | 299 | 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 += box | 295 | 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 := stBody | 299 | style := stBody |
modified
internal/ui/ui.go +1 -1 | @@ -132,7 +132,7 @@ func Notice(text string) { | ||
| 132 | 132 | n.Notice(text) |
| 133 | 133 | return |
| 134 | 134 | } |
| 135 | - fmt.Fprintln(Out, text) | |
| 135 | + _, _ = fmt.Fprintln(Out, text) | |
| 136 | 136 | } |
| 137 | 137 | |
| 138 | 138 | // 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 | return | 133 | 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: |