diff --git a/coderd/aitasks.go b/coderd/aitasks.go index f5d72beaf3903..9ba201f11c0d6 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -107,7 +107,7 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { return } - taskName := req.Name + taskName := taskname.GenerateFallback() if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" { anthropicModel := taskname.GetAnthropicModelFromEnv() diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index 8d12dd3a5ec95..d4fecd2145f6d 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -151,7 +151,6 @@ func TestTaskCreate(t *testing.T) { var ( ctx = testutil.Context(t, testutil.WaitShort) - taskName = "task-foo-bar-baz" taskPrompt = "Some task prompt" ) @@ -176,7 +175,6 @@ func TestTaskCreate(t *testing.T) { // When: We attempt to create a Task. workspace, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ - Name: taskName, TemplateVersionID: template.ActiveVersionID, Prompt: taskPrompt, }) @@ -184,7 +182,7 @@ func TestTaskCreate(t *testing.T) { coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) // Then: We expect a workspace to have been created. - assert.Equal(t, taskName, workspace.Name) + assert.NotEmpty(t, workspace.Name) assert.Equal(t, template.ID, workspace.TemplateID) // And: We expect it to have the "AI Prompt" parameter correctly set. @@ -201,7 +199,6 @@ func TestTaskCreate(t *testing.T) { var ( ctx = testutil.Context(t, testutil.WaitShort) - taskName = "task-foo-bar-baz" taskPrompt = "Some task prompt" ) @@ -217,7 +214,6 @@ func TestTaskCreate(t *testing.T) { // When: We attempt to create a Task. _, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ - Name: taskName, TemplateVersionID: template.ActiveVersionID, Prompt: taskPrompt, }) @@ -235,7 +231,6 @@ func TestTaskCreate(t *testing.T) { var ( ctx = testutil.Context(t, testutil.WaitShort) - taskName = "task-foo-bar-baz" taskPrompt = "Some task prompt" ) @@ -251,7 +246,6 @@ func TestTaskCreate(t *testing.T) { // When: We attempt to create a Task with an invalid template version ID. _, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ - Name: taskName, TemplateVersionID: uuid.New(), Prompt: taskPrompt, }) diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index 970e5ad67b2a0..dff57dfd0c7f5 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -2,11 +2,15 @@ package taskname import ( "context" + "fmt" "io" + "math/rand/v2" "os" + "strings" "github.com/anthropics/anthropic-sdk-go" anthropicoption "github.com/anthropics/anthropic-sdk-go/option" + "github.com/moby/moby/pkg/namesgenerator" "golang.org/x/xerrors" "github.com/coder/aisdk-go" @@ -20,19 +24,17 @@ const ( Requirements: - Only lowercase letters, numbers, and hyphens - Start with "task-" -- End with a random number between 0-99 -- Maximum 32 characters total +- Maximum 28 characters total - Descriptive of the main task Examples: -- "Help me debug a Python script" → "task-python-debug-12" -- "Create a React dashboard component" → "task-react-dashboard-93" -- "Analyze sales data from Q3" → "task-analyze-q3-sales-37" -- "Set up CI/CD pipeline" → "task-setup-cicd-44" +- "Help me debug a Python script" → "task-python-debug" +- "Create a React dashboard component" → "task-react-dashboard" +- "Analyze sales data from Q3" → "task-analyze-q3-sales" +- "Set up CI/CD pipeline" → "task-setup-cicd" If you cannot create a suitable name: -- Respond with "task-unnamed" -- Do not end with a random number` +- Respond with "task-unnamed"` ) var ( @@ -67,6 +69,32 @@ func GetAnthropicModelFromEnv() anthropic.Model { return anthropic.Model(os.Getenv("ANTHROPIC_MODEL")) } +// generateSuffix generates a random hex string between `0000` and `ffff`. +func generateSuffix() string { + numMin := 0x00000 + numMax := 0x10000 + //nolint:gosec // We don't need a cryptographically secure random number generator for generating a task name suffix. + num := rand.IntN(numMax-numMin) + numMin + + return fmt.Sprintf("%04x", num) +} + +func GenerateFallback() string { + // We have a 32 character limit for the name. + // We have a 5 character prefix `task-`. + // We have a 5 character suffix `-ffff`. + // This leaves us with 22 characters for the middle. + // + // Unfortunately, `namesgenerator.GetRandomName(0)` will + // generate names that are longer than 22 characters, so + // we just trim these down to length. + name := strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-") + name = name[:min(len(name), 22)] + name = strings.TrimSuffix(name, "-") + + return fmt.Sprintf("task-%s-%s", name, generateSuffix()) +} + func Generate(ctx context.Context, prompt string, opts ...Option) (string, error) { o := options{} for _, opt := range opts { @@ -127,7 +155,7 @@ func Generate(ctx context.Context, prompt string, opts ...Option) (string, error return "", ErrNoNameGenerated } - return generatedName, nil + return fmt.Sprintf("%s-%s", generatedName, generateSuffix()), nil } func anthropicDataStream(ctx context.Context, client anthropic.Client, model anthropic.Model, input []aisdk.Message) (aisdk.DataStream, error) { diff --git a/coderd/taskname/taskname_test.go b/coderd/taskname/taskname_test.go index 0737621b8f4eb..3eb26ef1d4ac7 100644 --- a/coderd/taskname/taskname_test.go +++ b/coderd/taskname/taskname_test.go @@ -15,6 +15,14 @@ const ( anthropicEnvVar = "ANTHROPIC_API_KEY" ) +func TestGenerateFallback(t *testing.T) { + t.Parallel() + + name := taskname.GenerateFallback() + err := codersdk.NameValid(name) + require.NoErrorf(t, err, "expected fallback to be valid workspace name, instead found %s", name) +} + func TestGenerateTaskName(t *testing.T) { t.Parallel() diff --git a/codersdk/aitasks.go b/codersdk/aitasks.go index 49d89bf5e2656..56b43d43a0d19 100644 --- a/codersdk/aitasks.go +++ b/codersdk/aitasks.go @@ -47,7 +47,6 @@ func (c *ExperimentalClient) AITaskPrompts(ctx context.Context, buildIDs []uuid. } type CreateTaskRequest struct { - Name string `json:"name"` TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"` TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` Prompt string `json:"prompt"` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4f873fb7b7829..db840040687fc 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -478,7 +478,6 @@ export interface CreateProvisionerKeyResponse { // From codersdk/aitasks.go export interface CreateTaskRequest { - readonly name: string; readonly template_version_id: string; readonly template_version_preset_id?: string; readonly prompt: string; diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 0e149f7943a61..b7b1d3f5998ef 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -53,7 +53,6 @@ import { useAuthenticated } from "hooks"; import { useExternalAuth } from "hooks/useExternalAuth"; import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react"; import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks"; -import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { type FC, type ReactNode, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; @@ -741,7 +740,6 @@ export const data = { } const workspace = await API.experimental.createTask(userId, { - name: `task-${generateWorkspaceName()}`, template_version_id: templateVersionId, template_version_preset_id: preset_id || undefined, prompt,