diff --git a/pkg/concurrent/slice.go b/pkg/concurrent/slice.go index 4b44a0724..a45ec5792 100644 --- a/pkg/concurrent/slice.go +++ b/pkg/concurrent/slice.go @@ -88,3 +88,10 @@ func (s *Slice[V]) Update(index int, f func(V) V) bool { s.values[index] = f(s.values[index]) return true } + +func (s *Slice[V]) Clear() { + s.mu.Lock() + defer s.mu.Unlock() + + s.values = nil +} diff --git a/pkg/tools/builtin/todo.go b/pkg/tools/builtin/todo.go index fad5d1b72..c7b340ab1 100644 --- a/pkg/tools/builtin/todo.go +++ b/pkg/tools/builtin/todo.go @@ -157,12 +157,32 @@ func (h *todoHandler) updateTodos(_ context.Context, params UpdateTodosArgs) (*t return tools.ResultError(output.String()), nil } + // Clear all todos if all are completed + if h.allCompleted() { + h.todos.Clear() + } + return &tools.ToolCallResult{ Output: output.String(), Meta: h.todos.All(), }, nil } +func (h *todoHandler) allCompleted() bool { + if h.todos.Length() == 0 { + return false + } + allDone := true + h.todos.Range(func(_ int, todo Todo) bool { + if todo.Status != "completed" { + allDone = false + return false + } + return true + }) + return allDone +} + func (h *todoHandler) listTodos(_ context.Context, _ tools.ToolCall) (*tools.ToolCallResult, error) { var output strings.Builder output.WriteString("Current todos:\n") diff --git a/pkg/tools/builtin/todo_test.go b/pkg/tools/builtin/todo_test.go index 899f05566..933e942b5 100644 --- a/pkg/tools/builtin/todo_test.go +++ b/pkg/tools/builtin/todo_test.go @@ -167,9 +167,9 @@ func TestTodoTool_UpdateTodos(t *testing.T) { func TestTodoTool_UpdateTodos_PartialFailure(t *testing.T) { tool := NewTodoTool() - // Create a single todo - _, err := tool.handler.createTodo(t.Context(), CreateTodoArgs{ - Description: "Test todo item", + // Create two todos so we can complete one without clearing the list + _, err := tool.handler.createTodos(t.Context(), CreateTodosArgs{ + Descriptions: []string{"First todo item", "Second todo item"}, }) require.NoError(t, err) @@ -185,10 +185,11 @@ func TestTodoTool_UpdateTodos_PartialFailure(t *testing.T) { assert.Contains(t, result.Output, "Updated 1 todos") assert.Contains(t, result.Output, "Not found: nonexistent") - // Verify the existing todo was updated + // Verify the existing todo was updated (list not cleared because todo_2 still pending) todos := tool.handler.todos.All() - require.Len(t, todos, 1) + require.Len(t, todos, 2) assert.Equal(t, "completed", todos[0].Status) + assert.Equal(t, "pending", todos[1].Status) } func TestTodoTool_UpdateTodos_AllNotFound(t *testing.T) { @@ -206,6 +207,35 @@ func TestTodoTool_UpdateTodos_AllNotFound(t *testing.T) { assert.Contains(t, result.Output, "Not found: nonexistent1, nonexistent2") } +func TestTodoTool_UpdateTodos_ClearsWhenAllCompleted(t *testing.T) { + tool := NewTodoTool() + + // Create multiple todos + _, err := tool.handler.createTodos(t.Context(), CreateTodosArgs{ + Descriptions: []string{"First todo item", "Second todo item"}, + }) + require.NoError(t, err) + + // Complete all todos + result, err := tool.handler.updateTodos(t.Context(), UpdateTodosArgs{ + Updates: []TodoUpdate{ + {ID: "todo_1", Status: "completed"}, + {ID: "todo_2", Status: "completed"}, + }, + }) + require.NoError(t, err) + assert.Contains(t, result.Output, "Updated 2 todos") + + // Verify all todos were cleared + todos := tool.handler.todos.All() + assert.Empty(t, todos) + + // Verify Meta is also empty + metaTodos, ok := result.Meta.([]Todo) + require.True(t, ok, "Meta should be []Todo") + assert.Empty(t, metaTodos) +} + func TestTodoTool_OutputSchema(t *testing.T) { tool := NewTodoTool()