Clear qlty lint findings: errcheck, dead MCP/plan code, staticcheck, shellcheck
Unchecked fmt.Fprint/Close results made explicit with blank assignments (the project's existing convention), test helpers check CreatePlan/NextTask errors, unused stubs in internal/tools removed, deprecated genkit.DefineModel replaced in tests, tagged switches, narrow nolint for the LSP MarkedString union servers still send. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4d8eac0 parent: 5b2f986 modified
02-build-releases.sh +1 -0 | @@ -134,6 +134,7 @@ echo " ✅ SHA256SUMS" | ||
| 134 | 134 | downloadTable() { |
| 135 | 135 | printf '| Platform | Download |\n|---|---|\n' |
| 136 | 136 | for platform in "${PLATFORMS[@]}"; do |
| 137 | + # shellcheck disable=SC2016 # the backticks are Markdown code spans, not a command substitution | |
| 137 | 138 | local goos="${platform%/*}" goarch="${platform#*/}" |
| 138 | 139 | printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")" |
| 139 | 140 | done |
| @@ -134,6 +134,7 @@ echo " ✅ SHA256SUMS" | |||
| 134 | downloadTable() { | 134 | 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 | + # shellcheck disable=SC2016 # the backticks are Markdown code spans, not a command substitution | ||
| 137 | local goos="${platform%/*}" goarch="${platform#*/}" | 138 | local goos="${platform%/*}" goarch="${platform#*/}" |
| 138 | printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")" | 139 | printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")" |
| 139 | done | 140 | done |
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" |
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/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/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 +16 -30 | @@ -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. |
| @@ -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. |
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: |