Files
cogito/maxattempts_test.go
Ettore Di Giacinto b4d85ec71b fix(tools): clamp WithMaxAttempts below 1 to 1 — a 0 silently skipped all tool execution
The tool-execution loops run `for range o.maxAttempts`, so maxAttempts=0
iterated zero times: the tool was never called and an EMPTY result was handed
back to the model with no error and no warning. A caller that forwards an unset
config field (e.g. AgentOptions.MaxAttempts) straight into WithMaxAttempts hits
this — the model then flies blind and hallucinates, with nothing in the logs to
explain why. Clamp anything below 1 to 1 (the documented default single attempt).

Found while smoke-testing nib's browser tool: the agent got empty tool results
because the test harness set MaxRetries but not MaxAttempts. Real callers that
set MaxAttempts>0 (e.g. Dante = 3) were unaffected, but the silent-no-op is a
footgun worth removing at the option boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8Z5dz1nVHAQUpjb5pgGCZ
2026-07-16 11:16:45 +00:00

27 lines
746 B
Go

package cogito
import "testing"
// TestWithMaxAttemptsClampsBelowOne locks the fix for a silent tool-execution
// no-op: the tool-run loops iterate `for range o.maxAttempts`, so a maxAttempts
// of 0 (a caller forwarding an unset config field into WithMaxAttempts) would
// run the tool zero times and hand the model an empty result with no error.
// WithMaxAttempts must clamp anything below 1 up to 1.
func TestWithMaxAttemptsClampsBelowOne(t *testing.T) {
cases := []struct {
in, want int
}{
{0, 1},
{-5, 1},
{1, 1},
{3, 3},
}
for _, c := range cases {
o := &Options{}
WithMaxAttempts(c.in)(o)
if o.maxAttempts != c.want {
t.Errorf("WithMaxAttempts(%d): maxAttempts=%d, want %d", c.in, o.maxAttempts, c.want)
}
}
}