From 7b943365c888ed5d0952c5601fd7ba15996c1cb9 Mon Sep 17 00:00:00 2001 From: Dennis Lanov Date: Sun, 11 Oct 2026 17:30:18 -0500 Subject: [PATCH] fix(think): protect concurrent tool calls with a mutex --- pkg/tools/builtin/think/think.go | 4 ++ pkg/tools/builtin/think/think_test.go | 68 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/pkg/tools/builtin/think/think.go b/pkg/tools/builtin/think/think.go index c6d6e26336..aaf370b65a 100644 --- a/pkg/tools/builtin/think/think.go +++ b/pkg/tools/builtin/think/think.go @@ -3,6 +3,7 @@ package think import ( "context" "strings" + "sync" "github.com/docker/docker-agent/pkg/tools" ) @@ -15,6 +16,7 @@ func CreateToolSet() (tools.ToolSet, error) { } type ToolSet struct { + mu sync.Mutex thoughts []string } @@ -23,6 +25,8 @@ type Args struct { } func (t *ToolSet) callTool(_ context.Context, params Args) (*tools.ToolCallResult, error) { + t.mu.Lock() + defer t.mu.Unlock() t.thoughts = append(t.thoughts, params.Thought) return tools.ResultSuccess("Thoughts:\n" + strings.Join(t.thoughts, "\n")), nil } diff --git a/pkg/tools/builtin/think/think_test.go b/pkg/tools/builtin/think/think_test.go index 446e7ac71b..64baa3ca7f 100644 --- a/pkg/tools/builtin/think/think_test.go +++ b/pkg/tools/builtin/think/think_test.go @@ -1,6 +1,10 @@ package think import ( + "context" + "fmt" + "strings" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -24,6 +28,70 @@ func TestThinkTool_Handler(t *testing.T) { assert.Contains(t, result.Output, "Another thought") } +func thinkHandler(t *testing.T) tools.ToolHandler { + t.Helper() + allTools, err := New().Tools(t.Context()) + require.NoError(t, err) + require.Len(t, allTools, 1) + return allTools[0].Handler +} + +func callThink(ctx context.Context, handler tools.ToolHandler, thought string) (string, error) { + result, err := handler(ctx, tools.ToolCall{ + Function: tools.FunctionCall{Name: ToolNameThink, Arguments: fmt.Sprintf(`{"thought":%q}`, thought)}, + }, tools.NopRuntime{}) + if err != nil { + return "", err + } + return result.Output, nil +} + +func TestThinkTool_SequentialOutput(t *testing.T) { + t.Parallel() + handler := thinkHandler(t) + + output, err := callThink(t.Context(), handler, "first") + require.NoError(t, err) + assert.Equal(t, "Thoughts:\nfirst", output) + + output, err = callThink(t.Context(), handler, "second") + require.NoError(t, err) + assert.Equal(t, "Thoughts:\nfirst\nsecond", output) +} + +func TestThinkTool_ConcurrentCalls(t *testing.T) { + t.Parallel() + handler := thinkHandler(t) + + const n = 64 + thoughts := make([]string, n) + outputs := make([]string, n) + errs := make([]error, n) + var wg sync.WaitGroup + for i := range n { + thoughts[i] = fmt.Sprintf("thought-%02d", i) + wg.Go(func() { + outputs[i], errs[i] = callThink(t.Context(), handler, thoughts[i]) + }) + } + wg.Wait() + + for i := range n { + require.NoError(t, errs[i]) + lines := strings.Split(outputs[i], "\n") + assert.Equal(t, "Thoughts:", lines[0]) + assert.Contains(t, lines[1:], thoughts[i]) + } + + output, err := callThink(t.Context(), handler, "final") + require.NoError(t, err) + lines := strings.Split(output, "\n") + require.Len(t, lines, n+2) + assert.Equal(t, "Thoughts:", lines[0]) + assert.ElementsMatch(t, thoughts, lines[1:n+1]) + assert.Equal(t, "final", lines[n+1]) +} + func TestThinkTool_OutputSchema(t *testing.T) { t.Parallel() tool := New()